From 01380bd4080105836300d8158ff1aa4f0544c939 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 6 Dec 2023 17:07:26 +0100 Subject: [PATCH 01/91] Add flyway migration script for adding "alias_id" and "alias_zid" columns to identity_provider table --- .../db/postgresql/V4_105__Identity_Provider_Add_Alias.sql | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V4_105__Identity_Provider_Add_Alias.sql diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V4_105__Identity_Provider_Add_Alias.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V4_105__Identity_Provider_Add_Alias.sql new file mode 100644 index 00000000000..ffb7da387ba --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V4_105__Identity_Provider_Add_Alias.sql @@ -0,0 +1,5 @@ +-- add columns for alias-id and alias-zone-id +ALTER TABLE identity_provider + ADD COLUMN alias_id VARCHAR(36) DEFAULT NULL; +ALTER TABLE identity_provider + ADD COLUMN alias_zid VARCHAR(36) DEFAULT NULL; \ No newline at end of file From b67d39c090bbde286a18aedae38368a45ef1ee42 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 7 Dec 2023 14:52:30 +0100 Subject: [PATCH 02/91] Add aliasId and aliasZid properties to IdentityProvider class --- .../uaa/provider/IdentityProvider.java | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProvider.java b/model/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProvider.java index 0fd7b76d18d..cde3ab482da 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProvider.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProvider.java @@ -56,7 +56,10 @@ public class IdentityProvider { public static final String FIELD_IDENTITY_ZONE_ID = "identityZoneId"; public static final String FIELD_CONFIG = "config"; public static final String FIELD_TYPE = "type"; + public static final String FIELD_ALIAS_ID = "alias_id"; + public static final String FIELD_ALIAS_ZID = "alias_zid"; //see deserializer at the bottom + private String id; @NotNull private String originKey; @@ -71,7 +74,10 @@ public class IdentityProvider { private Date lastModified = new Date(); private boolean active = true; private String identityZoneId; - + @JsonProperty("alias_id") + private String aliasId; + @JsonProperty("alias_zid") + private String aliasZid; public Date getCreated() { return created; } @@ -197,6 +203,24 @@ public IdentityProvider setIdentityZoneId(String identityZoneId) { return this; } + public String getAliasId() { + return aliasId; + } + + public IdentityProvider setAliasId(String aliasId) { + this.aliasId = aliasId; + return this; + } + + public String getAliasZid() { + return aliasZid; + } + + public IdentityProvider setAliasZid(String aliasZid) { + this.aliasZid = aliasZid; + return this; + } + @Override public int hashCode() { final int prime = 31; @@ -208,6 +232,8 @@ public int hashCode() { result = prime * result + ((name == null) ? 0 : name.hashCode()); result = prime * result + ((originKey == null) ? 0 : originKey.hashCode()); result = prime * result + ((type == null) ? 0 : type.hashCode()); + result = prime * result + ((aliasId == null) ? 0 : aliasId.hashCode()); + result = prime * result + ((aliasZid == null) ? 0 : aliasZid.hashCode()); result = prime * result + version; return result; } @@ -256,6 +282,20 @@ public boolean equals(Object obj) { return false; } else if (!type.equals(other.type)) return false; + if (aliasId == null) { + if (other.aliasId != null) { + return false; + } + } else if (!aliasId.equals(other.aliasId)) { + return false; + } + if (aliasZid == null) { + if (other.aliasZid != null) { + return false; + } + } else if (!aliasZid.equals(other.aliasZid)) { + return false; + } if (version != other.version) return false; return true; @@ -269,6 +309,21 @@ public String toString() { sb.append(", name='").append(name).append('\''); sb.append(", type='").append(type).append('\''); sb.append(", active=").append(active); + + sb.append(", aliasId="); + if (aliasId != null) { + sb.append('\'').append(aliasId).append('\''); + } else { + sb.append("null"); + } + + sb.append(", aliasZid="); + if (aliasZid != null) { + sb.append('\'').append(aliasZid).append('\''); + } else { + sb.append("null"); + } + sb.append('}'); return sb.toString(); } @@ -304,6 +359,8 @@ public void serialize(IdentityProvider value, JsonGenerator gen, SerializerProvi writeDateField(FIELD_LAST_MODIFIED, value.getLastModified(), gen); gen.writeBooleanField(FIELD_ACTIVE, value.isActive()); gen.writeStringField(FIELD_IDENTITY_ZONE_ID, value.getIdentityZoneId()); + gen.writeStringField(FIELD_ALIAS_ID, value.getAliasId()); + gen.writeStringField(FIELD_ALIAS_ZID, value.getAliasZid()); gen.writeEndObject(); } @@ -369,6 +426,8 @@ public IdentityProvider deserialize(JsonParser jp, DeserializationContext ctxt) result.setLastModified(getNodeAsDate(node, FIELD_LAST_MODIFIED)); result.setActive(getNodeAsBoolean(node, FIELD_ACTIVE, true)); result.setIdentityZoneId(getNodeAsString(node, FIELD_IDENTITY_ZONE_ID, null)); + result.setAliasId(getNodeAsString(node, FIELD_ALIAS_ID, null)); + result.setAliasZid(getNodeAsString(node, FIELD_ALIAS_ZID, null)); return result; } From a0cfe807cf6e4914d1e3eb4fa1a4ec27ff24f54b Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 11 Dec 2023 16:13:10 +0100 Subject: [PATCH 03/91] Fix IdentityProviderEndpointDocs --- .../uaa/mock/providers/IdentityProviderEndpointDocs.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java index 45cd5ccf382..80a364d254b 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java @@ -99,6 +99,8 @@ class IdentityProviderEndpointDocs extends EndpointDocs { private static final FieldDescriptor ID = fieldWithPath("id").type(STRING).description(ID_DESC); private static final FieldDescriptor CREATED = fieldWithPath("created").description(CREATED_DESC); private static final FieldDescriptor LAST_MODIFIED = fieldWithPath("last_modified").description(LAST_MODIFIED_DESC); + private static final FieldDescriptor ALIAS_ZID = fieldWithPath("alias_zid").optional().type(STRING).description("The ID of the identity zone to which this IdP should be mirrored"); + private static final FieldDescriptor ALIAS_ID = fieldWithPath("alias_id").optional().type(STRING).description("The ID of the mirrored IdP"); private static final FieldDescriptor GROUP_WHITELIST = fieldWithPath("config.externalGroupsWhitelist").optional(null).type(ARRAY).description("JSON Array containing the groups names which need to be populated in the user's `id_token` or response from `/userinfo` endpoint. If you don't specify the whitelist no groups will be populated in the `id_token` or `/userinfo` response." + "
Please note that regex is allowed. Acceptable patterns are" + "
  • `*` translates to all groups
  • " + @@ -121,7 +123,9 @@ class IdentityProviderEndpointDocs extends EndpointDocs { EMAIL_DOMAIN, ACTIVE, ADD_SHADOW_USER, - STORE_CUSTOM_ATTRIBUTES + STORE_CUSTOM_ATTRIBUTES, + ALIAS_ID, + ALIAS_ZID }; private FieldDescriptor[] attributeMappingFields = { From c9f4a21c5577d4ba56e619f015ab3e8c0fd0ef76 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Tue, 12 Dec 2023 13:44:47 +0100 Subject: [PATCH 04/91] Add migration scripts for MySQL and HSQL --- .../uaa/db/hsqldb/V4_105__Identity_Provider_Add_Alias.sql | 5 +++++ .../uaa/db/mysql/V4_105__Identity_Provider_Add_Alias.sql | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V4_105__Identity_Provider_Add_Alias.sql create mode 100644 server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_105__Identity_Provider_Add_Alias.sql diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V4_105__Identity_Provider_Add_Alias.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V4_105__Identity_Provider_Add_Alias.sql new file mode 100644 index 00000000000..ffb7da387ba --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V4_105__Identity_Provider_Add_Alias.sql @@ -0,0 +1,5 @@ +-- add columns for alias-id and alias-zone-id +ALTER TABLE identity_provider + ADD COLUMN alias_id VARCHAR(36) DEFAULT NULL; +ALTER TABLE identity_provider + ADD COLUMN alias_zid VARCHAR(36) DEFAULT NULL; \ No newline at end of file diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_105__Identity_Provider_Add_Alias.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_105__Identity_Provider_Add_Alias.sql new file mode 100644 index 00000000000..ffb7da387ba --- /dev/null +++ b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_105__Identity_Provider_Add_Alias.sql @@ -0,0 +1,5 @@ +-- add columns for alias-id and alias-zone-id +ALTER TABLE identity_provider + ADD COLUMN alias_id VARCHAR(36) DEFAULT NULL; +ALTER TABLE identity_provider + ADD COLUMN alias_zid VARCHAR(36) DEFAULT NULL; \ No newline at end of file From 7d7aef55948c309cb23c33e0251caaaf78efd9b5 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Tue, 12 Dec 2023 13:49:05 +0100 Subject: [PATCH 05/91] Fix queries creating or modifying identity providers --- .../JdbcIdentityProviderProvisioning.java | 18 ++++++++++++------ .../identity/uaa/test/TestUtils.java | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) 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 1b6aa275d1e..064382f48df 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 @@ -26,15 +26,15 @@ public class JdbcIdentityProviderProvisioning implements IdentityProviderProvisi private static Logger logger = LoggerFactory.getLogger(JdbcIdentityProviderProvisioning.class); - public static final String ID_PROVIDER_FIELDS = "id,version,created,lastmodified,name,origin_key,type,config,identity_zone_id,active"; + public static final String ID_PROVIDER_FIELDS = "id,version,created,lastmodified,name,origin_key,type,config,identity_zone_id,active,alias_id,alias_zid"; - public static final String CREATE_IDENTITY_PROVIDER_SQL = "insert into identity_provider(" + ID_PROVIDER_FIELDS + ") values (?,?,?,?,?,?,?,?,?,?)"; + public static final String CREATE_IDENTITY_PROVIDER_SQL = "insert into identity_provider(" + ID_PROVIDER_FIELDS + ") values (?,?,?,?,?,?,?,?,?,?,?,?)"; public static final String IDENTITY_PROVIDERS_QUERY = "select " + ID_PROVIDER_FIELDS + " from identity_provider where identity_zone_id=?"; public static final String IDENTITY_ACTIVE_PROVIDERS_QUERY = IDENTITY_PROVIDERS_QUERY + " and active=?"; - public static final String ID_PROVIDER_UPDATE_FIELDS = "version,lastmodified,name,type,config,active".replace(",", "=?,") + "=?"; + public static final String ID_PROVIDER_UPDATE_FIELDS = "version,lastmodified,name,type,config,active,alias_id,alias_zid".replace(",", "=?,") + "=?"; public static final String UPDATE_IDENTITY_PROVIDER_SQL = "update identity_provider set " + ID_PROVIDER_UPDATE_FIELDS + " where id=? and identity_zone_id=?"; @@ -101,7 +101,9 @@ public IdentityProvider create(final IdentityProvider identityProvider, String z ps.setString(pos++, identityProvider.getType()); ps.setString(pos++, JsonUtils.writeValueAsString(identityProvider.getConfig())); ps.setString(pos++, zoneId); - ps.setBoolean(pos, identityProvider.isActive()); + ps.setBoolean(pos++, identityProvider.isActive()); + ps.setString(pos++, identityProvider.getAliasId()); + ps.setString(pos, identityProvider.getAliasZid()); }); } catch (DuplicateKeyException e) { throw new IdpAlreadyExistsException(e.getMostSpecificCause().getMessage()); @@ -121,7 +123,9 @@ public IdentityProvider update(final IdentityProvider identityProvider, String z ps.setString(pos++, JsonUtils.writeValueAsString(identityProvider.getConfig())); ps.setBoolean(pos++, identityProvider.isActive()); ps.setString(pos++, identityProvider.getId().trim()); - ps.setString(pos, zoneId); + ps.setString(pos++, zoneId); + ps.setString(pos++, identityProvider.getAliasId()); + ps.setString(pos, identityProvider.getAliasZid()); }); return retrieve(identityProvider.getId(), zoneId); } @@ -200,7 +204,9 @@ public IdentityProvider mapRow(ResultSet rs, int rowNum) throws SQLException { } } identityProvider.setIdentityZoneId(rs.getString(pos++)); - identityProvider.setActive(rs.getBoolean(pos)); + identityProvider.setActive(rs.getBoolean(pos++)); + identityProvider.setAliasId(rs.getString(pos++)); + identityProvider.setAliasZid(rs.getString(pos)); return identityProvider; } } diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/test/TestUtils.java b/server/src/test/java/org/cloudfoundry/identity/uaa/test/TestUtils.java index 5061654097f..ac9bdc5ff51 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/test/TestUtils.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/test/TestUtils.java @@ -95,7 +95,7 @@ private static void seedUaaZoneSimilarToHowTheRealFlywayMigrationDoesIt(JdbcTemp for (String origin : origins) { String identityProviderId = UUID.randomUUID().toString(); originMap.put(origin, identityProviderId); - jdbcTemplate.update("insert into identity_provider VALUES (?,?,?,0,?,?,?,?,null,?)",identityProviderId, t, t, uaa.getId(),origin,origin,origin,true); + jdbcTemplate.update("insert into identity_provider VALUES (?,?,?,0,?,?,?,?,null,?,null,null)",identityProviderId, t, t, uaa.getId(),origin,origin,origin,true); } jdbcTemplate.update("update oauth_client_details set identity_zone_id = ?",uaa.getId()); List clientIds = jdbcTemplate.queryForList("SELECT client_id from oauth_client_details", String.class); From e150510c4443fc10a257410cb7f9330ae1939ea2 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Tue, 12 Dec 2023 14:13:50 +0100 Subject: [PATCH 06/91] Fix optional constraints in IdP endpoint docs --- .../IdentityProviderEndpointDocs.java | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java index 80a364d254b..2e911561e58 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java @@ -61,6 +61,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -99,8 +100,8 @@ class IdentityProviderEndpointDocs extends EndpointDocs { private static final FieldDescriptor ID = fieldWithPath("id").type(STRING).description(ID_DESC); private static final FieldDescriptor CREATED = fieldWithPath("created").description(CREATED_DESC); private static final FieldDescriptor LAST_MODIFIED = fieldWithPath("last_modified").description(LAST_MODIFIED_DESC); - private static final FieldDescriptor ALIAS_ZID = fieldWithPath("alias_zid").optional().type(STRING).description("The ID of the identity zone to which this IdP should be mirrored"); - private static final FieldDescriptor ALIAS_ID = fieldWithPath("alias_id").optional().type(STRING).description("The ID of the mirrored IdP"); + private static final FieldDescriptor ALIAS_ZID = fieldWithPath("alias_zid").description("The ID of the identity zone to which this IdP should be mirrored").attributes(key("constraints").value("Optional")).optional().type(STRING); + private static final FieldDescriptor ALIAS_ID = fieldWithPath("alias_id").description("The ID of the mirrored IdP").attributes(key("constraints").value("Optional")).optional().type(STRING); private static final FieldDescriptor GROUP_WHITELIST = fieldWithPath("config.externalGroupsWhitelist").optional(null).type(ARRAY).description("JSON Array containing the groups names which need to be populated in the user's `id_token` or response from `/userinfo` endpoint. If you don't specify the whitelist no groups will be populated in the `id_token` or `/userinfo` response." + "
    Please note that regex is allowed. Acceptable patterns are" + "
    • `*` translates to all groups
    • " + @@ -363,24 +364,30 @@ void createSAMLIdentityProvider() throws Exception { IdentityProvider identityProvider = getSamlProvider("SAML"); identityProvider.setSerializeConfigRaw(true); - FieldDescriptor[] idempotentFields = (FieldDescriptor[]) ArrayUtils.addAll(commonProviderFields, ArrayUtils.addAll(new FieldDescriptor[]{ - fieldWithPath("type").required().description("`saml`"), - fieldWithPath("originKey").required().description("A unique alias for the SAML provider"), - SKIP_SSL_VALIDATION, - STORE_CUSTOM_ATTRIBUTES, - fieldWithPath("config.metaDataLocation").required().type(STRING).description("SAML Metadata - either an XML string or a URL that will deliver XML content"), - fieldWithPath("config.nameID").optional(null).type(STRING).description("The name ID to use for the username, default is \"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\"."), - fieldWithPath("config.assertionConsumerIndex").optional(null).type(NUMBER).description("SAML assertion consumer index, default is 0"), - fieldWithPath("config.metadataTrustCheck").optional(null).type(BOOLEAN).description("Should metadata be validated, defaults to false"), - fieldWithPath("config.showSamlLink").optional(null).type(BOOLEAN).description("Should the SAML login link be displayed on the login page, defaults to false"), - fieldWithPath("config.linkText").constrained("Required if the ``showSamlLink`` is set to true").type(STRING).description("The link text for the SAML IDP on the login page"), - fieldWithPath("config.groupMappingMode").optional(ExternalGroupMappingMode.EXPLICITLY_MAPPED).type(STRING).description("Either ``EXPLICITLY_MAPPED`` in order to map external groups to OAuth scopes using the group mappings, or ``AS_SCOPES`` to use SAML group names as scopes."), - fieldWithPath("config.iconUrl").optional(null).type(STRING).description("Reserved for future use"), - fieldWithPath("config.socketFactoryClassName").optional(null).description("Property is deprecated and value is ignored."), - fieldWithPath("config.authnContext").optional(null).type(ARRAY).description("List of AuthnContextClassRef to include in the SAMLRequest. If not specified no AuthnContext will be requested."), - EXTERNAL_GROUPS_WHITELIST, - fieldWithPath("config.attributeMappings.user_name").optional("NameID").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for SAML is `NameID`."), - }, attributeMappingFields)); + FieldDescriptor[] idempotentFields = (FieldDescriptor[]) ArrayUtils.addAll( + commonProviderFields, + ArrayUtils.addAll( + new FieldDescriptor[]{ + fieldWithPath("type").required().description("`saml`"), + fieldWithPath("originKey").required().description("A unique alias for the SAML provider"), + SKIP_SSL_VALIDATION, + STORE_CUSTOM_ATTRIBUTES, + fieldWithPath("config.metaDataLocation").required().type(STRING).description("SAML Metadata - either an XML string or a URL that will deliver XML content"), + fieldWithPath("config.nameID").optional(null).type(STRING).description("The name ID to use for the username, default is \"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\"."), + fieldWithPath("config.assertionConsumerIndex").optional(null).type(NUMBER).description("SAML assertion consumer index, default is 0"), + fieldWithPath("config.metadataTrustCheck").optional(null).type(BOOLEAN).description("Should metadata be validated, defaults to false"), + fieldWithPath("config.showSamlLink").optional(null).type(BOOLEAN).description("Should the SAML login link be displayed on the login page, defaults to false"), + fieldWithPath("config.linkText").constrained("Required if the ``showSamlLink`` is set to true").type(STRING).description("The link text for the SAML IDP on the login page"), + fieldWithPath("config.groupMappingMode").optional(ExternalGroupMappingMode.EXPLICITLY_MAPPED).type(STRING).description("Either ``EXPLICITLY_MAPPED`` in order to map external groups to OAuth scopes using the group mappings, or ``AS_SCOPES`` to use SAML group names as scopes."), + fieldWithPath("config.iconUrl").optional(null).type(STRING).description("Reserved for future use"), + fieldWithPath("config.socketFactoryClassName").optional(null).description("Property is deprecated and value is ignored."), + fieldWithPath("config.authnContext").optional(null).type(ARRAY).description("List of AuthnContextClassRef to include in the SAMLRequest. If not specified no AuthnContext will be requested."), + EXTERNAL_GROUPS_WHITELIST, + fieldWithPath("config.attributeMappings.user_name").optional("NameID").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for SAML is `NameID`."), + }, + attributeMappingFields + ) + ); Snippet requestFields = requestFields(idempotentFields); @@ -772,6 +779,8 @@ void getAllIdentityProviders() throws Exception { fieldWithPath("[].originKey").description("Unique identifier for the identity provider."), fieldWithPath("[].name").description(NAME_DESC), fieldWithPath("[].config").description(CONFIG_DESCRIPTION), + fieldWithPath("[].alias_id").description("TODO"), + fieldWithPath("[].alias_zid").description("TODO"), fieldWithPath("[].version").description(VERSION_DESC), fieldWithPath("[].active").description(ACTIVE_DESC), From 06a3081e2fa3ebd15704f7ba94e54c920403282d Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Tue, 12 Dec 2023 15:00:39 +0100 Subject: [PATCH 07/91] Fix row mapper for update in JdbcIdentityProviderProvisioning --- .../JdbcIdentityProviderProvisioning.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) 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 064382f48df..ca5b7624d8b 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 @@ -116,16 +116,20 @@ public IdentityProvider update(final IdentityProvider identityProvider, String z validate(identityProvider); jdbcTemplate.update(UPDATE_IDENTITY_PROVIDER_SQL, ps -> { int pos = 1; - ps.setInt(pos++, identityProvider.getVersion() + 1); - ps.setTimestamp(pos++, new Timestamp(new Date().getTime())); - ps.setString(pos++, identityProvider.getName()); - ps.setString(pos++, identityProvider.getType()); - ps.setString(pos++, JsonUtils.writeValueAsString(identityProvider.getConfig())); - ps.setBoolean(pos++, identityProvider.isActive()); - ps.setString(pos++, identityProvider.getId().trim()); - ps.setString(pos++, zoneId); - ps.setString(pos++, identityProvider.getAliasId()); - ps.setString(pos, identityProvider.getAliasZid()); + + // placeholders in SELECT + ps.setInt(pos++, identityProvider.getVersion() + 1); // version + ps.setTimestamp(pos++, new Timestamp(new Date().getTime())); // lastmodified + ps.setString(pos++, identityProvider.getName()); // name + ps.setString(pos++, identityProvider.getType()); // type + ps.setString(pos++, JsonUtils.writeValueAsString(identityProvider.getConfig())); // config + ps.setBoolean(pos++, identityProvider.isActive()); // active + ps.setString(pos++, identityProvider.getAliasId()); // alias ID + ps.setString(pos++, identityProvider.getAliasZid()); // alias ZID + + // placeholders in WHERE + ps.setString(pos++, identityProvider.getId().trim()); // id + ps.setString(pos, zoneId); // identity_zone_id }); return retrieve(identityProvider.getId(), zoneId); } From b6dca9d59730dca48bcf9fb9b7b6c5c479b1bfe5 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Tue, 12 Dec 2023 15:08:48 +0100 Subject: [PATCH 08/91] Fix field definitions for alias_id and alias_zid in IdentityProviderEndpointDocs --- .../IdentityProviderEndpointDocs.java | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java index 2e911561e58..850e6fb8346 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java @@ -79,6 +79,8 @@ class IdentityProviderEndpointDocs extends EndpointDocs { private static final String FAMILY_NAME_DESC = "Map `family_name` to the attribute for family name in the provider assertion or token."; private static final String PHONE_NUMBER_DESC = "Map `phone_number` to the attribute for phone number in the provider assertion or token."; private static final String GIVEN_NAME_DESC = "Map `given_name` to the attribute for given name in the provider assertion or token."; + private static final String ALIAS_ID_DESC = "The ID of the mirrored IdP"; + private static final String ALIAS_ZID_DESC = "The ID of the identity zone to which this IdP should be mirrored"; private static final FieldDescriptor STORE_CUSTOM_ATTRIBUTES = fieldWithPath("config.storeCustomAttributes").optional(true).type(BOOLEAN).description("Set to true, to store custom user attributes to be fetched from the /userinfo endpoint"); private static final FieldDescriptor SKIP_SSL_VALIDATION = fieldWithPath("config.skipSslValidation").optional(false).type(BOOLEAN).description("Set to true, to skip SSL validation when fetching metadata."); @@ -100,8 +102,8 @@ class IdentityProviderEndpointDocs extends EndpointDocs { private static final FieldDescriptor ID = fieldWithPath("id").type(STRING).description(ID_DESC); private static final FieldDescriptor CREATED = fieldWithPath("created").description(CREATED_DESC); private static final FieldDescriptor LAST_MODIFIED = fieldWithPath("last_modified").description(LAST_MODIFIED_DESC); - private static final FieldDescriptor ALIAS_ZID = fieldWithPath("alias_zid").description("The ID of the identity zone to which this IdP should be mirrored").attributes(key("constraints").value("Optional")).optional().type(STRING); - private static final FieldDescriptor ALIAS_ID = fieldWithPath("alias_id").description("The ID of the mirrored IdP").attributes(key("constraints").value("Optional")).optional().type(STRING); + private static final FieldDescriptor ALIAS_ID = fieldWithPath("alias_id").description(ALIAS_ID_DESC).attributes(key("constraints").value("Optional")).optional().type(STRING); + private static final FieldDescriptor ALIAS_ZID = fieldWithPath("alias_zid").description(ALIAS_ZID_DESC).attributes(key("constraints").value("Optional")).optional().type(STRING); private static final FieldDescriptor GROUP_WHITELIST = fieldWithPath("config.externalGroupsWhitelist").optional(null).type(ARRAY).description("JSON Array containing the groups names which need to be populated in the user's `id_token` or response from `/userinfo` endpoint. If you don't specify the whitelist no groups will be populated in the `id_token` or `/userinfo` response." + "
      Please note that regex is allowed. Acceptable patterns are" + "
      • `*` translates to all groups
      • " + @@ -364,30 +366,24 @@ void createSAMLIdentityProvider() throws Exception { IdentityProvider identityProvider = getSamlProvider("SAML"); identityProvider.setSerializeConfigRaw(true); - FieldDescriptor[] idempotentFields = (FieldDescriptor[]) ArrayUtils.addAll( - commonProviderFields, - ArrayUtils.addAll( - new FieldDescriptor[]{ - fieldWithPath("type").required().description("`saml`"), - fieldWithPath("originKey").required().description("A unique alias for the SAML provider"), - SKIP_SSL_VALIDATION, - STORE_CUSTOM_ATTRIBUTES, - fieldWithPath("config.metaDataLocation").required().type(STRING).description("SAML Metadata - either an XML string or a URL that will deliver XML content"), - fieldWithPath("config.nameID").optional(null).type(STRING).description("The name ID to use for the username, default is \"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\"."), - fieldWithPath("config.assertionConsumerIndex").optional(null).type(NUMBER).description("SAML assertion consumer index, default is 0"), - fieldWithPath("config.metadataTrustCheck").optional(null).type(BOOLEAN).description("Should metadata be validated, defaults to false"), - fieldWithPath("config.showSamlLink").optional(null).type(BOOLEAN).description("Should the SAML login link be displayed on the login page, defaults to false"), - fieldWithPath("config.linkText").constrained("Required if the ``showSamlLink`` is set to true").type(STRING).description("The link text for the SAML IDP on the login page"), - fieldWithPath("config.groupMappingMode").optional(ExternalGroupMappingMode.EXPLICITLY_MAPPED).type(STRING).description("Either ``EXPLICITLY_MAPPED`` in order to map external groups to OAuth scopes using the group mappings, or ``AS_SCOPES`` to use SAML group names as scopes."), - fieldWithPath("config.iconUrl").optional(null).type(STRING).description("Reserved for future use"), - fieldWithPath("config.socketFactoryClassName").optional(null).description("Property is deprecated and value is ignored."), - fieldWithPath("config.authnContext").optional(null).type(ARRAY).description("List of AuthnContextClassRef to include in the SAMLRequest. If not specified no AuthnContext will be requested."), - EXTERNAL_GROUPS_WHITELIST, - fieldWithPath("config.attributeMappings.user_name").optional("NameID").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for SAML is `NameID`."), - }, - attributeMappingFields - ) - ); + FieldDescriptor[] idempotentFields = (FieldDescriptor[]) ArrayUtils.addAll(commonProviderFields, ArrayUtils.addAll(new FieldDescriptor[]{ + fieldWithPath("type").required().description("`saml`"), + fieldWithPath("originKey").required().description("A unique alias for the SAML provider"), + SKIP_SSL_VALIDATION, + STORE_CUSTOM_ATTRIBUTES, + fieldWithPath("config.metaDataLocation").required().type(STRING).description("SAML Metadata - either an XML string or a URL that will deliver XML content"), + fieldWithPath("config.nameID").optional(null).type(STRING).description("The name ID to use for the username, default is \"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\"."), + fieldWithPath("config.assertionConsumerIndex").optional(null).type(NUMBER).description("SAML assertion consumer index, default is 0"), + fieldWithPath("config.metadataTrustCheck").optional(null).type(BOOLEAN).description("Should metadata be validated, defaults to false"), + fieldWithPath("config.showSamlLink").optional(null).type(BOOLEAN).description("Should the SAML login link be displayed on the login page, defaults to false"), + fieldWithPath("config.linkText").constrained("Required if the ``showSamlLink`` is set to true").type(STRING).description("The link text for the SAML IDP on the login page"), + fieldWithPath("config.groupMappingMode").optional(ExternalGroupMappingMode.EXPLICITLY_MAPPED).type(STRING).description("Either ``EXPLICITLY_MAPPED`` in order to map external groups to OAuth scopes using the group mappings, or ``AS_SCOPES`` to use SAML group names as scopes."), + fieldWithPath("config.iconUrl").optional(null).type(STRING).description("Reserved for future use"), + fieldWithPath("config.socketFactoryClassName").optional(null).description("Property is deprecated and value is ignored."), + fieldWithPath("config.authnContext").optional(null).type(ARRAY).description("List of AuthnContextClassRef to include in the SAMLRequest. If not specified no AuthnContext will be requested."), + EXTERNAL_GROUPS_WHITELIST, + fieldWithPath("config.attributeMappings.user_name").optional("NameID").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for SAML is `NameID`."), + }, attributeMappingFields)); Snippet requestFields = requestFields(idempotentFields); @@ -779,8 +775,8 @@ void getAllIdentityProviders() throws Exception { fieldWithPath("[].originKey").description("Unique identifier for the identity provider."), fieldWithPath("[].name").description(NAME_DESC), fieldWithPath("[].config").description(CONFIG_DESCRIPTION), - fieldWithPath("[].alias_id").description("TODO"), - fieldWithPath("[].alias_zid").description("TODO"), + fieldWithPath("[].alias_id").description(ALIAS_ID_DESC), + fieldWithPath("[].alias_zid").description(ALIAS_ZID), fieldWithPath("[].version").description(VERSION_DESC), fieldWithPath("[].active").description(ACTIVE_DESC), From 6a8ef062f0d7bb57880f116f26e783caef8ef609 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 13 Dec 2023 15:06:14 +0100 Subject: [PATCH 09/91] Improve comments in JdbcIdentityProviderProvisioning --- .../JdbcIdentityProviderProvisioning.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 ca5b7624d8b..1fe515e9875 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 @@ -117,19 +117,19 @@ public IdentityProvider update(final IdentityProvider identityProvider, String z jdbcTemplate.update(UPDATE_IDENTITY_PROVIDER_SQL, ps -> { int pos = 1; - // placeholders in SELECT - ps.setInt(pos++, identityProvider.getVersion() + 1); // version + // placeholders in INSERT INTO + ps.setInt(pos++, identityProvider.getVersion() + 1); ps.setTimestamp(pos++, new Timestamp(new Date().getTime())); // lastmodified - ps.setString(pos++, identityProvider.getName()); // name - ps.setString(pos++, identityProvider.getType()); // type - ps.setString(pos++, JsonUtils.writeValueAsString(identityProvider.getConfig())); // config - ps.setBoolean(pos++, identityProvider.isActive()); // active - ps.setString(pos++, identityProvider.getAliasId()); // alias ID - ps.setString(pos++, identityProvider.getAliasZid()); // alias ZID + ps.setString(pos++, identityProvider.getName()); + ps.setString(pos++, identityProvider.getType()); + ps.setString(pos++, JsonUtils.writeValueAsString(identityProvider.getConfig())); + ps.setBoolean(pos++, identityProvider.isActive()); + ps.setString(pos++, identityProvider.getAliasId()); + ps.setString(pos++, identityProvider.getAliasZid()); // placeholders in WHERE - ps.setString(pos++, identityProvider.getId().trim()); // id - ps.setString(pos, zoneId); // identity_zone_id + ps.setString(pos++, identityProvider.getId().trim()); + ps.setString(pos, zoneId); }); return retrieve(identityProvider.getId(), zoneId); } From 6522e825c2acac59a273a83aa155d12ebd943cdb Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 18 Dec 2023 12:57:26 +0100 Subject: [PATCH 10/91] Add tests for IdP creation and update affecting alias properties --- ...IdentityProviderEndpointsMockMvcTests.java | 542 +++++++++++++++++- .../identity/uaa/mock/util/MockMvcUtils.java | 4 + 2 files changed, 541 insertions(+), 5 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsMockMvcTests.java index 760b51417fa..ece75d685b7 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsMockMvcTests.java @@ -13,7 +13,9 @@ package org.cloudfoundry.identity.uaa.mock.providers; import com.fasterxml.jackson.core.type.TypeReference; + import org.apache.commons.lang.RandomStringUtils; +import org.assertj.core.api.Assertions; import org.cloudfoundry.identity.uaa.DefaultTestContext; import org.cloudfoundry.identity.uaa.audit.AuditEventType; import org.cloudfoundry.identity.uaa.constants.OriginKeys; @@ -28,6 +30,7 @@ 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.util.UaaTokenUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter; @@ -35,11 +38,16 @@ import org.cloudfoundry.identity.uaa.zone.event.IdentityProviderModifiedEvent; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.http.HttpStatus; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.security.oauth2.provider.client.BaseClientDetails; @@ -53,7 +61,12 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.*; +import java.util.stream.Stream; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.*; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LDAP; import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.USER_NAME_ATTRIBUTE_NAME; import static org.hamcrest.Matchers.containsString; @@ -119,14 +132,14 @@ void test_delete_through_event() throws Exception { IdentityProviderBootstrap bootstrap = webApplicationContext.getBean(IdentityProviderBootstrap.class); assertNotNull(identityProviderProvisioning.retrieveByOrigin(origin, IdentityZone.getUaaZoneId())); try { - bootstrap.setOriginsToDelete(Collections.singletonList(origin)); + bootstrap.setOriginsToDelete(singletonList(origin)); bootstrap.onApplicationEvent(new ContextRefreshedEvent(webApplicationContext)); } finally { bootstrap.setOriginsToDelete(null); } try { identityProviderProvisioning.retrieveByOrigin(origin, IdentityZone.getUaaZoneId()); - fail("Identity provider should have been deleted"); + Assertions.fail("Identity provider should have been deleted"); } catch (EmptyResultDataAccessException ignored) { } } @@ -211,6 +224,525 @@ void test_Create_and_Delete_SamlProvider() throws Exception { ).andExpect(status().isNotFound()); } + @Nested + class AliasTests { + private IdentityZone customZone; + + @BeforeEach + void setUp() throws Exception { + customZone = MockMvcUtils.createZoneUsingWebRequest(mockMvc, identityToken); + } + + @Nested + class Create { + @Test + void testCreate_SuccessCase_MirrorFromUaaZoneToCustomZone() throws Exception { + testCreate_SuccessCase(IdentityZone.getUaa(), customZone); + } + + @Test + void testCreate_SuccessCase_MirrorFromCustomZoneToUaaZone() throws Exception { + testCreate_SuccessCase(customZone, IdentityZone.getUaa()); + } + + @Test + void testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual_Uaa() throws Exception { + testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual(IdentityZone.getUaa()); + } + + @Test + void testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual_Custom() throws Exception { + testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual(customZone); + } + + @Test + void testCreate_ShouldReject_WhenNeitherIdzNorAliasZidIsUaa() throws Exception { + final IdentityZone otherCustomZone = MockMvcUtils.createZoneUsingWebRequest(mockMvc, identityToken); + final IdentityProvider provider = buildIdpWithAliasProperties( + customZone.getId(), + null, + otherCustomZone.getId() + ); + testCreate_ShouldReject(customZone, provider, HttpStatus.UNPROCESSABLE_ENTITY); + } + + @Test + void testCreate_ShouldReject_WhenAliasIdIsSet() throws Exception { + testCreate_ShouldReject( + customZone, + buildIdpWithAliasProperties( + customZone.getId(), + UUID.randomUUID().toString(), + IdentityZone.getUaaZoneId() + ), + HttpStatus.UNPROCESSABLE_ENTITY + ); + } + + @Test + void testCreate_ShouldReject_WhenIdzReferencedInAliasZidDoesNotExist() throws Exception { + final IdentityProvider provider = buildIdpWithAliasProperties( + IdentityZone.getUaaZoneId(), + null, + UUID.randomUUID().toString() // does not exist + ); + final IdentityZone zone = IdentityZone.getUaa(); + testCreate_ShouldReject(zone, provider, HttpStatus.UNPROCESSABLE_ENTITY); + } + + @Test + void testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone_CustomToUaa() throws Exception { + testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone( + customZone, + IdentityZone.getUaa() + ); + } + + @Test + void testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone_UaaToCustom() throws Exception { + testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone( + IdentityZone.getUaa(), + customZone + ); + } + + private void testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone( + final IdentityZone zone1, + final IdentityZone zone2 + ) throws Exception { + final String originKey = RandomStringUtils.randomAlphabetic(10); + + // create IdP with origin key in custom zone + final IdentityProvider createdIdp1 = createIdp( + zone1, + buildIdpWithAliasProperties(zone1.getId(), null, null, originKey), + getAccessTokenForZone(zone1) + ); + assertNotNull(createdIdp1); + + // then, create an IdP in the "uaa" zone with the same origin key that should be mirrored to the custom zone + testCreate_ShouldReject( + zone2, + buildIdpWithAliasProperties(zone2.getId(), null, zone1.getId(), originKey), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + private void testCreate_SuccessCase(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + assertNotNull(zone1); + assertNotNull(zone2); + + // build IdP in zone1 with aliasZid set to zone2 + final IdentityProvider provider = buildIdpWithAliasProperties( + IdentityZone.getUaa().getId(), + null, + zone2.getId() + ); + + // create IdP in zone1 + final IdentityProvider originalIdp = createIdp(zone1, provider, getAccessTokenForZone(zone1)); + assertNotNull(originalIdp); + assertTrue(StringUtils.hasText(originalIdp.getAliasId())); + assertTrue(StringUtils.hasText(originalIdp.getAliasZid())); + assertEquals(zone2.getId(), originalIdp.getAliasZid()); + + // read mirrored IdP from zone2 + final String accessTokenZone2 = getAccessTokenForZone(zone2); + final IdentityProvider mirroredIdp = readIdpFromZone(zone2, originalIdp.getAliasId(), accessTokenZone2); + assertIdpReferencesOtherIdp(mirroredIdp, originalIdp); + assertOtherPropertiesAreEqual(originalIdp, mirroredIdp); + + // check if aliasId in first IdP is equal to the ID of the mirrored one + assertEquals(mirroredIdp.getId(), originalIdp.getAliasId()); + } + + private void testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual(final IdentityZone zone) throws Exception { + final IdentityProvider provider = buildIdpWithAliasProperties( + zone.getId(), + null, + zone.getId() + ); + testCreate_ShouldReject(zone, provider, HttpStatus.UNPROCESSABLE_ENTITY); + } + + private void testCreate_ShouldReject( + final IdentityZone zone, + final IdentityProvider idp, + final HttpStatus expectedStatus + ) throws Exception { + assertNotNull(zone); + assertNotNull(idp); + + // create IdP in zone + final MvcResult result = createIdpAndReturnResult(zone, idp, getAccessTokenForZone(zone)); + assertThat(result.getResponse().getStatus()).isEqualTo(expectedStatus.value()); + } + } + + @Nested + class Update { + @Test + void testUpdate_Success_MigrationScenario_CreateMirroredIdp_UaaToCustomZone() throws Exception { + testUpdate_MigrationScenario_ShouldCreateMirroredIdp(IdentityZone.getUaa(), customZone); + } + + @Test + void testUpdate_Success_MigrationScenario_CreateMirroredIdp_CustomToUaaZone() throws Exception { + testUpdate_MigrationScenario_ShouldCreateMirroredIdp(customZone, IdentityZone.getUaa()); + } + + @Test + void testUpdate_Success_OtherPropertiesOfAlreadyMirroredIdpAreChanged() throws Exception { + final IdentityZone zone1 = IdentityZone.getUaa(); + final IdentityZone zone2 = customZone; + + // create a mirrored IdP + final IdentityProvider originalIdp = createMirroredIdp(zone1, zone2); + + // update other property + final String newName = "new name"; + originalIdp.setName(newName); + final IdentityProvider updatedOriginalIdp = updateIdp(zone1, originalIdp, getAccessTokenForZone(zone1)); + assertNotNull(updatedOriginalIdp); + assertNotNull(updatedOriginalIdp.getAliasId()); + assertNotNull(updatedOriginalIdp.getAliasZid()); + assertEquals(zone2.getId(), updatedOriginalIdp.getAliasZid()); + + assertNotNull(updatedOriginalIdp.getName()); + assertEquals(newName, updatedOriginalIdp.getName()); + + // check if the change is propagated to the mirrored IdP + final String accessTokenZone2 = getAccessTokenForZone(zone2); + final IdentityProvider mirroredIdp = readIdpFromZone( + zone2, + updatedOriginalIdp.getAliasId(), + accessTokenZone2 + ); + assertIdpReferencesOtherIdp(mirroredIdp, updatedOriginalIdp); + assertNotNull(mirroredIdp.getName()); + assertEquals(newName, mirroredIdp.getName()); + } + + @ParameterizedTest + @MethodSource + void testUpdate_ShouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp( + final String newAliasId, + final String newAliasZid + ) throws Exception { + final IdentityProvider originalIdp = createMirroredIdp(IdentityZone.getUaa(), customZone); + originalIdp.setAliasId(newAliasId); + originalIdp.setAliasZid(newAliasZid); + updateIdp_ShouldReject(IdentityZone.getUaa(), originalIdp, HttpStatus.UNPROCESSABLE_ENTITY); + } + + private static Stream testUpdate_ShouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp() { + return Stream.of(null, "", "other").flatMap(aliasIdValue -> + Stream.of(null, "", "other").map(aliasZidValue -> + Arguments.of(aliasIdValue, aliasZidValue) + )); + } + + @Test + void testUpdate_ShouldReject_OnlyAliasIdSet_Uaa() throws Exception { + testUpdate_ShouldReject_OnlyAliasIdSet(IdentityZone.getUaa()); + } + + @Test + void testUpdate_ShouldReject_OnlyAliasIdSet_Custom() throws Exception { + testUpdate_ShouldReject_OnlyAliasIdSet(customZone); + } + + private void testUpdate_ShouldReject_OnlyAliasIdSet(final IdentityZone zone) throws Exception { + final IdentityProvider idp = buildIdpWithAliasProperties(zone.getId(), null, null); + final IdentityProvider createdProvider = createIdp(zone, idp, getAccessTokenForZone(zone)); + assertNull(createdProvider.getAliasZid()); + createdProvider.setAliasId(UUID.randomUUID().toString()); + updateIdp_ShouldReject(zone, createdProvider, HttpStatus.UNPROCESSABLE_ENTITY); + } + + @Test + void testUpdate_ShouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone() throws Exception { + final String originKey = RandomStringUtils.randomAlphabetic(10); + + final IdentityProvider existingProviderInCustomZone = buildIdpWithAliasProperties( + customZone.getId(), + null, + null, + originKey + ); + createIdp(customZone, existingProviderInCustomZone, getAccessTokenForZone(customZone)); + + final IdentityZone zone = IdentityZone.getUaa(); + final IdentityProvider idp = buildIdpWithAliasProperties( + IdentityZone.getUaa().getId(), + null, + null, + originKey // same origin key + ); + // same origin key + final IdentityProvider providerInUaaZone = createIdp(zone, idp, getAccessTokenForZone(zone)); + + providerInUaaZone.setAliasZid(customZone.getId()); + updateIdp_ShouldReject( + IdentityZone.getUaa(), + providerInUaaZone, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + @Test + void testUpdate_ShouldReject_IdpInCustomZoneMirroredToOtherCustomZone() throws Exception { + final IdentityProvider idpInCustomZone = createIdp( + customZone, + buildIdpWithAliasProperties(customZone.getId(), null, null), + getAccessTokenForZone(customZone) + ); + + // try to mirror it to another custom zone + idpInCustomZone.setAliasZid("not-uaa"); + updateIdp_ShouldReject( + customZone, + idpInCustomZone, + HttpStatus.UNPROCESSABLE_ENTITY + ); + } + + private IdentityProvider createMirroredIdp( + final IdentityZone zone1, + final IdentityZone zone2 + ) throws Exception { + final IdentityProvider provider = buildIdpWithAliasProperties( + zone1.getId(), + null, + zone2.getId() + ); + return createIdp(zone1, provider, getAccessTokenForZone(zone1)); + } + + private void testUpdate_MigrationScenario_ShouldCreateMirroredIdp( + final IdentityZone zone1, + final IdentityZone zone2 + ) throws Exception { + final String accessTokenForZone1 = getAccessTokenForZone(zone1); + + // create regular idp without alias properties in UAA zone + final IdentityProvider existingIdpWithoutAlias = createIdp( + zone1, + buildIdpWithAliasProperties(zone1.getId(), null, null), + accessTokenForZone1 + ); + assertNotNull(existingIdpWithoutAlias); + assertNotNull(existingIdpWithoutAlias.getId()); + + // perform update: set Alias ZID + existingIdpWithoutAlias.setAliasZid(zone2.getId()); + final IdentityProvider idpAfterUpdate = updateIdp( + zone1, + existingIdpWithoutAlias, + accessTokenForZone1 + ); + assertNotNull(idpAfterUpdate.getAliasId()); + assertNotNull(idpAfterUpdate.getAliasZid()); + assertEquals(zone2.getId(), idpAfterUpdate.getAliasZid()); + + // read mirrored IdP through alias id in original IdP + final String accessTokenForZone2 = getAccessTokenForZone(zone2); + final IdentityProvider mirroredIdp = readIdpFromZone( + zone2, + idpAfterUpdate.getAliasId(), + accessTokenForZone2 + ); + assertIdpReferencesOtherIdp(mirroredIdp, idpAfterUpdate); + assertOtherPropertiesAreEqual(idpAfterUpdate, mirroredIdp); + } + + private IdentityProvider updateIdp( + final IdentityZone zone, + final IdentityProvider updatePayload, + final String accessTokenForZone + ) throws Exception { + updatePayload.setIdentityZoneId(zone.getId()); + final MvcResult result = updateIdpAndReturnResult(zone, updatePayload, accessTokenForZone); + assertEquals(HttpStatus.OK.value(), result.getResponse().getStatus()); + + final IdentityProvider originalIdpAfterUpdate = JsonUtils.readValue( + result.getResponse().getContentAsString(), + IdentityProvider.class + ); + assertNotNull(originalIdpAfterUpdate); + assertNotNull(originalIdpAfterUpdate.getIdentityZoneId()); + assertEquals(zone.getId(), originalIdpAfterUpdate.getIdentityZoneId()); + return originalIdpAfterUpdate; + } + + private MvcResult updateIdpAndReturnResult( + final IdentityZone zone, + final IdentityProvider updatePayload, + final String accessTokenForZone + ) throws Exception { + final String id = updatePayload.getId(); + assertThat(id).isNotNull().isNotBlank(); + + final MockHttpServletRequestBuilder updateRequestBuilder = put("/identity-providers/" + id) + .header("Authorization", "Bearer " + accessTokenForZone) + .header(IdentityZoneSwitchingFilter.HEADER, zone.getId()) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(updatePayload)); + return mockMvc.perform(updateRequestBuilder).andReturn(); + } + + private void updateIdp_ShouldReject( + final IdentityZone zone, + final IdentityProvider idp, + final HttpStatus expectedStatusCode + ) throws Exception { + final MvcResult result = updateIdpAndReturnResult(zone,idp, getAccessTokenForZone(zone)); + assertThat(result.getResponse().getStatus()).isEqualTo(expectedStatusCode.value()); + } + } + + private void assertIdpReferencesOtherIdp(final IdentityProvider idp, final IdentityProvider referencedIdp) { + assertNotNull(idp); + assertNotNull(referencedIdp); + assertTrue(StringUtils.hasText(idp.getAliasId())); + assertEquals(referencedIdp.getId(), idp.getAliasId()); + assertTrue(StringUtils.hasText(idp.getAliasZid())); + assertEquals(referencedIdp.getIdentityZoneId(), idp.getAliasZid()); + } + + private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final IdentityProvider mirroredIdp) { + // apart from the zone ID, the configs should be identical + final SamlIdentityProviderDefinition originalIdpConfig = (SamlIdentityProviderDefinition) idp.getConfig(); + originalIdpConfig.setZoneId(null); + final SamlIdentityProviderDefinition mirroredIdpConfig = (SamlIdentityProviderDefinition) mirroredIdp.getConfig(); + mirroredIdpConfig.setZoneId(null); + assertEquals(originalIdpConfig, mirroredIdpConfig); + + // check if remaining properties are equal + assertEquals(idp.getOriginKey(), mirroredIdp.getOriginKey()); + assertEquals(idp.getName(), mirroredIdp.getName()); + assertEquals(idp.getType(), mirroredIdp.getType()); + } + + private IdentityProvider createIdp( + final IdentityZone zone, + final IdentityProvider provider, + final String accessTokenForZone + ) throws Exception { + final MvcResult createResult = createIdpAndReturnResult(zone, provider, accessTokenForZone); + assertThat(createResult.getResponse().getStatus()).isEqualTo(HttpStatus.CREATED.value()); + return JsonUtils.readValue( + createResult.getResponse().getContentAsString(), + IdentityProvider.class + ); + } + + private MvcResult createIdpAndReturnResult( + final IdentityZone zone, + final IdentityProvider idp, + final String accessTokenForZone + ) throws Exception { + final MockHttpServletRequestBuilder createRequestBuilder = post("/identity-providers") + .param("rawConfig", "true") + .header("Authorization", "Bearer " + accessTokenForZone) + .header(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER, zone.getSubdomain()) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(idp)); + return mockMvc.perform(createRequestBuilder).andReturn(); + } + + private String getAccessTokenForZone(final IdentityZone zone) throws Exception { + final List scopesForZone = getScopesForZone(zone, "admin"); + + final ScimUser adminUser = MockMvcUtils.createAdminForZone( + mockMvc, + adminToken, + String.join(",", scopesForZone), + IdentityZone.getUaaZoneId() + ); + final String accessToken = MockMvcUtils.getUserOAuthAccessTokenAuthCode( + mockMvc, + "identity", + "identitysecret", + adminUser.getId(), + adminUser.getUserName(), + adminUser.getPassword(), + String.join(" ", scopesForZone), + IdentityZone.getUaaZoneId() + ); + eventListener.clearEvents(); + + // check if the token contains the expected scopes + final Map claims = UaaTokenUtils.getClaims(accessToken); + assertTrue(claims.containsKey("scope")); + assertTrue(claims.get("scope") instanceof List); + final List resultingScopes = (List) claims.get("scope"); + assertThat(resultingScopes).hasSameElementsAs(scopesForZone); + + return accessToken; + } + + private IdentityProvider readIdpFromZone( + final IdentityZone zone, + final String id, + final String accessToken + ) throws Exception { + final MockHttpServletRequestBuilder getRequestBuilder = get("/identity-providers/" + id) + .param("rawConfig", "true") + .header(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER, zone.getSubdomain()) + .header("Authorization", "Bearer " + accessToken); + final MvcResult getResult = mockMvc.perform(getRequestBuilder) + .andExpect(status().isOk()) + .andReturn(); + return JsonUtils.readValue( + getResult.getResponse().getContentAsString(), + IdentityProvider.class + ); + } + + private static List getScopesForZone(final IdentityZone zone, final String... scopes) { + return Stream.of(scopes).map(scope -> String.format("zones.%s.%s", zone.getId(), scope)).collect(toList()); + } + + private static IdentityProvider buildIdpWithAliasProperties( + final String idzId, + final String aliasId, + final String aliasZid + ) { + final String originKey = RandomStringUtils.randomAlphabetic(8); + return buildIdpWithAliasProperties(idzId, aliasId, aliasZid, originKey); + } + + private static IdentityProvider buildIdpWithAliasProperties( + final String idzId, + final String aliasId, + final String aliasZid, + final String originKey + ) { + final String metadata = String.format( + BootstrapSamlIdentityProviderDataTests.xmlWithoutID, + "http://localhost:9999/metadata/" + originKey + ); + final SamlIdentityProviderDefinition samlDefinition = new SamlIdentityProviderDefinition() + .setMetaDataLocation(metadata) + .setLinkText("Test SAML Provider"); + samlDefinition.setEmailDomain(Arrays.asList("test.com", "test2.com")); + samlDefinition.setExternalGroupsWhitelist(singletonList("value")); + samlDefinition.setAttributeMappings(singletonMap("given_name", "first_name")); + + final IdentityProvider provider = new IdentityProvider<>(); + provider.setActive(true); + provider.setName(originKey); + provider.setIdentityZoneId(idzId); + provider.setType(OriginKeys.SAML); + provider.setOriginKey(originKey); + provider.setConfig(samlDefinition); + provider.setAliasId(aliasId); + provider.setAliasZid(aliasZid); + return provider; + } + } + @Test void test_delete_with_invalid_id_returns_404() throws Exception { String accessToken = setUpAccessToken(); @@ -234,7 +766,7 @@ void test_delete_response_not_containing_relying_party_secret() throws Exception definition.setRelyingPartySecret("secret"); definition.setShowLinkText(false); definition.setUserPropagationParameter("username"); - definition.setExternalGroupsWhitelist(Collections.singletonList("uaa.user")); + definition.setExternalGroupsWhitelist(singletonList("uaa.user")); List prompts = Arrays.asList(new Prompt("username", "text", "Email"), new Prompt("password", "password", "Password"), new Prompt("passcode", "password", "Temporary Authentication Code (Get on at /passcode)")); @@ -251,7 +783,7 @@ void test_delete_response_not_containing_relying_party_secret() throws Exception MvcResult result = mockMvc.perform(requestBuilder).andExpect(status().isOk()).andReturn(); IdentityProvider returnedIdentityProvider = JsonUtils.readValue( result.getResponse().getContentAsString(), IdentityProvider.class); - assertNull(((AbstractExternalOAuthIdentityProviderDefinition)returnedIdentityProvider.getConfig()) + assertNull(((AbstractExternalOAuthIdentityProviderDefinition) returnedIdentityProvider.getConfig()) .getRelyingPartySecret()); } @@ -320,7 +852,7 @@ void test_delete_response_not_containing_bind_password() throws Exception { IdentityProvider returnedIdentityProvider = JsonUtils.readValue( deleteResult.getResponse().getContentAsString(), IdentityProvider.class); - assertNull(((LdapIdentityProviderDefinition)returnedIdentityProvider. + assertNull(((LdapIdentityProviderDefinition) returnedIdentityProvider. getConfig()).getBindPassword()); } } 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 a89ff0602bb..0e400be3280 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 @@ -977,6 +977,10 @@ public static BaseClientDetails createClient(MockMvc mockMvc, String accessToken return createClient(mockMvc, accessToken, clientDetails, IdentityZone.getUaa(), status().isCreated()); } + public static BaseClientDetails createClient(MockMvc mockMvc, IdentityZone identityZone, String accessToken, BaseClientDetails clientDetails) throws Exception { + return createClient(mockMvc, accessToken, clientDetails, identityZone, status().isCreated()); + } + public static void deleteClient(MockMvc mockMvc, String accessToken, String clientId, String zoneSubdomain) throws Exception { MockHttpServletRequestBuilder createClientDelete = delete("/oauth/clients/" + clientId) .header("Authorization", "Bearer " + accessToken) From 9f19be97c94bfde34f0d7a734b646d976d1ed3bf Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 18 Dec 2023 13:04:33 +0100 Subject: [PATCH 11/91] Add logic for mirroring IdPs from/to custom zones --- .../provider/IdentityProviderEndpoints.java | 216 ++++++++++++++++-- .../provider/IdpMirroringFailedException.java | 9 + .../IdentityProviderEndpointsTest.java | 8 + 3 files changed, 219 insertions(+), 14 deletions(-) create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpMirroringFailedException.java 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 804dafb2e8c..6459b74b139 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 @@ -13,6 +13,8 @@ */ package org.cloudfoundry.identity.uaa.provider; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; +import org.cloudfoundry.identity.uaa.zone.ZoneDoesNotExistsException; import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,7 +39,11 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionException; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; +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; @@ -53,6 +59,7 @@ import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LDAP; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OIDC10; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.CONFLICT; import static org.springframework.http.HttpStatus.CREATED; @@ -60,6 +67,7 @@ import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; import static org.springframework.http.HttpStatus.OK; import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; +import static org.springframework.util.StringUtils.hasText; import static org.springframework.web.bind.annotation.RequestMethod.DELETE; import static org.springframework.web.bind.annotation.RequestMethod.GET; import static org.springframework.web.bind.annotation.RequestMethod.PATCH; @@ -79,6 +87,9 @@ public class IdentityProviderEndpoints implements ApplicationEventPublisherAware private final SamlIdentityProviderConfigurator samlConfigurator; private final IdentityProviderConfigValidator configValidator; private final IdentityZoneManager identityZoneManager; + private final IdentityZoneProvisioning identityZoneProvisioning; + private final TransactionTemplate transactionTemplate; + private ApplicationEventPublisher publisher = null; @Override @@ -92,13 +103,18 @@ public IdentityProviderEndpoints( final @Qualifier("scimGroupProvisioning") ScimGroupProvisioning scimGroupProvisioning, final @Qualifier("metaDataProviders") SamlIdentityProviderConfigurator samlConfigurator, final @Qualifier("identityProviderConfigValidator") IdentityProviderConfigValidator configValidator, - final IdentityZoneManager identityZoneManager) { + final IdentityZoneManager identityZoneManager, + final @Qualifier("identityZoneProvisioning") IdentityZoneProvisioning identityZoneProvisioning, + final @Qualifier("transactionManager") PlatformTransactionManager transactionManager + ) { this.identityProviderProvisioning = identityProviderProvisioning; this.scimGroupExternalMembershipManager = scimGroupExternalMembershipManager; this.scimGroupProvisioning = scimGroupProvisioning; this.samlConfigurator = samlConfigurator; this.configValidator = configValidator; this.identityZoneManager = identityZoneManager; + this.identityZoneProvisioning = identityZoneProvisioning; + this.transactionTemplate = new TransactionTemplate(transactionManager); } @RequestMapping(method = POST) @@ -119,26 +135,148 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden samlConfigurator.validateSamlIdentityProviderDefinition(definition); body.setConfig(definition); } + + // at this point, the alias ID must not be set + if (hasText(body.getAliasId())) { + logger.debug("IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "] - Alias ID was not null."); + return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); + } + + if (hasText(body.getAliasZid())) { + // check if the zone exists + try { + identityZoneProvisioning.retrieve(body.getAliasZid()); + } catch (final ZoneDoesNotExistsException e) { + logger.debug("IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "] - Zone referenced in alias zone ID does not exist."); + return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); + } + + // mirroring is only allowed from or to the "uaa" zone + if (!zoneId.equals(UAA) && !body.getAliasZid().equals(UAA)) { + logger.debug("IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "] - Invalid: Alias ZID set to custom zone, IdP created in custom zone."); + return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); + } + + // mirroring cannot be done to the same zone + if (body.getAliasZid().equals(zoneId)) { + logger.debug("IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "] - Invalid: Alias ZID equal to current IdZ."); + return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); + } + } + + // persist IdP and mirror if necessary + final IdentityProvider createdIdp; try { - IdentityProvider createdIdp = identityProviderProvisioning.create(body, zoneId); - createdIdp.setSerializeConfigRaw(rawConfig); - redactSensitiveData(createdIdp); - return new ResponseEntity<>(createdIdp, CREATED); - } catch (IdpAlreadyExistsException e) { - return new ResponseEntity<>(body, CONFLICT); - } catch (Exception x) { - logger.error("Unable to create IdentityProvider[origin="+body.getOriginKey()+"; zone="+body.getIdentityZoneId()+"]", x); + createdIdp = transactionTemplate.execute(txStatus -> { + final IdentityProvider createdOriginalIdp = identityProviderProvisioning.create(body, zoneId); + createdOriginalIdp.setSerializeConfigRaw(rawConfig); + redactSensitiveData(createdOriginalIdp); + + return ensureConsistencyOfMirroredIdp(createdOriginalIdp); + }); + } catch (final TransactionException e) { + if (e.getCause() instanceof IdpAlreadyExistsException) { + return new ResponseEntity<>(body, CONFLICT); + } + + logger.error("Unable to create IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "]", e); return new ResponseEntity<>(body, INTERNAL_SERVER_ERROR); } + + return new ResponseEntity<>(createdIdp, CREATED); + } + + /** + * Ensure consistency with a mirrored IdP referenced in the original IdPs alias properties. If the IdP has both its + * alias ID and alias ZID set, the existing mirrored IdP is updated. If only the alias ZID is set, a new mirrored + * IdP is created. + * This method should be executed in a transaction together with the original create or update operation. + * The method assumes that + * + * @param originalIdp the original IdP; must be persisted, i.e., have an ID, already + * @return the original IdP after the operation, with a potentially updated "aliasId" field + * @throws IdpMirroringFailedException if a new mirrored IdP needs to be created, but the zone referenced in + * 'aliasZid' does not exist + * @throws IdpMirroringFailedException if 'aliasId' and 'aliasZid' are set in the original IdP, but the referenced + * mirrored IdP could not be found + */ + private IdentityProvider ensureConsistencyOfMirroredIdp(final IdentityProvider originalIdp) throws IdpMirroringFailedException { + if (!hasText(originalIdp.getAliasZid())) { + // no mirroring is necessary + return originalIdp; + } + + final IdentityProvider mirroredIdp = new IdentityProvider<>() + .setActive(originalIdp.isActive()) + .setConfig(originalIdp.getConfig()) + .setName(originalIdp.getName()) + .setOriginKey(originalIdp.getOriginKey()) + .setType(originalIdp.getType()) + // reference the ID and zone ID of the initial IdP entry + .setAliasZid(originalIdp.getIdentityZoneId()) + .setAliasId(originalIdp.getId()) + .setIdentityZoneId(originalIdp.getAliasZid()); + mirroredIdp.setSerializeConfigRaw(originalIdp.isSerializeConfigRaw()); + + if (hasText(originalIdp.getAliasId())) { + // retrieve and update existing mirrored IdP + final IdentityProvider existingMirroredIdp; + try { + existingMirroredIdp = identityProviderProvisioning.retrieve( + originalIdp.getAliasId(), + originalIdp.getAliasZid() + ); + } catch (final EmptyResultDataAccessException e) { + throw new IdpMirroringFailedException(String.format( + "The IdP referenced in the 'aliasId' and 'aliasZid' properties of IdP '%s' does not exist.", + originalIdp.getId() + ), e); + } + mirroredIdp.setId(existingMirroredIdp.getId()); + identityProviderProvisioning.update(mirroredIdp, originalIdp.getAliasZid()); + return originalIdp; + } + + // check if IdZ referenced in 'aliasZid' exists + try { + identityZoneProvisioning.retrieve(originalIdp.getAliasZid()); + } catch (final ZoneDoesNotExistsException e) { + throw new IdpMirroringFailedException(String.format( + "Could not mirror IdP '%s' to zone '%s', as zone does not exist.", + originalIdp.getId(), + originalIdp.getAliasZid() + ), e); + } + + // create new mirrored IdP in alias zid + final IdentityProvider persistedMirroredIdp = identityProviderProvisioning.create( + mirroredIdp, + originalIdp.getAliasZid() + ); + + // update alias ID in original IdP + originalIdp.setAliasId(persistedMirroredIdp.getId()); + return identityProviderProvisioning.update(originalIdp, originalIdp.getIdentityZoneId()); } @RequestMapping(value = "{id}", method = DELETE) @Transactional public ResponseEntity deleteIdentityProvider(@PathVariable String id, @RequestParam(required = false, defaultValue = "false") boolean rawConfig) { - IdentityProvider existing = identityProviderProvisioning.retrieve(id, identityZoneManager.getCurrentIdentityZoneId()); + String identityZoneId = identityZoneManager.getCurrentIdentityZoneId(); + IdentityProvider existing = identityProviderProvisioning.retrieve(id, identityZoneId); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // delete mirrored IdP if alias fields are set + if (existing != null && StringUtils.hasText(existing.getAliasZid()) && StringUtils.hasText(existing.getAliasId())) { + IdentityProvider mirroredIdp = identityProviderProvisioning.retrieve(existing.getAliasId(), existing.getAliasZid()); + mirroredIdp.setSerializeConfigRaw(rawConfig); + publisher.publishEvent(new EntityDeletedEvent<>(mirroredIdp, authentication, identityZoneId)); + } + if (publisher!=null && existing!=null) { existing.setSerializeConfigRaw(rawConfig); - publisher.publishEvent(new EntityDeletedEvent<>(existing, SecurityContextHolder.getContext().getAuthentication(), identityZoneManager.getCurrentIdentityZoneId())); + publisher.publishEvent(new EntityDeletedEvent<>(existing, authentication, identityZoneId)); redactSensitiveData(existing); return new ResponseEntity<>(existing, OK); } else { @@ -146,7 +284,6 @@ public ResponseEntity deleteIdentityProvider(@PathVariable Str } } - @RequestMapping(value = "{id}", method = PUT) public ResponseEntity updateIdentityProvider(@PathVariable String id, @RequestBody IdentityProvider body, @RequestParam(required = false, defaultValue = "false") boolean rawConfig) throws MetadataProviderException { body.setSerializeConfigRaw(rawConfig); @@ -161,6 +298,12 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str logger.debug("IdentityProvider[origin="+body.getOriginKey()+"; zone="+body.getIdentityZoneId()+"] - Configuration validation error for update.", e); return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } + + if (!isValidAliasPropertyUpdate(body, existing)) { + logger.error("IdentityProvider[origin="+body.getOriginKey()+"; zone="+body.getIdentityZoneId()+"] - Alias ID and/or ZID changed during update of already mirrored IdP."); + return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); + } + if (OriginKeys.SAML.equals(body.getType())) { body.setOriginKey(existing.getOriginKey()); //we do not allow origin to change for a SAML provider, since that can cause clashes SamlIdentityProviderDefinition definition = ObjectUtils.castInstance(body.getConfig(), SamlIdentityProviderDefinition.class); @@ -169,12 +312,57 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str samlConfigurator.validateSamlIdentityProviderDefinition(definition); body.setConfig(definition); } - IdentityProvider updatedIdp = identityProviderProvisioning.update(body, zoneId); + + final IdentityProvider updatedIdp = transactionTemplate.execute(txStatus -> { + final IdentityProvider updatedOriginalIdp = identityProviderProvisioning.update(body, zoneId); + return ensureConsistencyOfMirroredIdp(updatedOriginalIdp); + }); + updatedIdp.setSerializeConfigRaw(rawConfig); redactSensitiveData(updatedIdp); return new ResponseEntity<>(updatedIdp, OK); } + /** + * Checks whether an update operation is valid in regard to the alias properties. + * + * @param updatePayload the updated version of the IdP to be persisted + * @param existingIdp the existing version of the IdP + * @return whether the update of the alias properties is valid + */ + private static boolean isValidAliasPropertyUpdate( + final IdentityProvider updatePayload, + final IdentityProvider existingIdp + ) { + if (!hasText(existingIdp.getAliasId()) && !hasText(existingIdp.getAliasZid())) { + // no alias properties set previously + + if (hasText(updatePayload.getAliasId())) { + return false; // 'aliasId' must be empty + } + + if (!hasText(updatePayload.getAliasZid())) { + return true; // no mirroring necessary + } + + // one of the zones must be "uaa" + return updatePayload.getAliasZid().equals(UAA) || updatePayload.getIdentityZoneId().equals(UAA); + } + + if (!hasText(existingIdp.getAliasId()) || !hasText(existingIdp.getAliasZid())) { + // at this point, we expect both properties to be set -> if not, the IdP is in an inconsistent state + throw new IllegalStateException(String.format( + "Both alias ID and alias ZID expected to be set for IdP '%s' in zone '%s'.", + existingIdp.getId(), + existingIdp.getIdentityZoneId() + )); + } + + // both properties must be equal in the update payload + return existingIdp.getAliasId().equals(updatePayload.getAliasId()) + && existingIdp.getAliasZid().equals(updatePayload.getAliasZid()); + } + @RequestMapping (value = "{id}/status", method = PATCH) public ResponseEntity updateIdentityProviderStatus(@PathVariable String id, @RequestBody IdentityProviderStatus body) { String zoneId = identityZoneManager.getCurrentIdentityZoneId(); @@ -183,7 +371,7 @@ public ResponseEntity updateIdentityProviderStatus(@Path logger.debug("Invalid payload. The property requirePasswordChangeRequired needs to be set"); return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } - if(!OriginKeys.UAA.equals(existing.getType())) { + if(!UAA.equals(existing.getType())) { logger.debug("Invalid operation. This operation is not supported on external IDP"); return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpMirroringFailedException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpMirroringFailedException.java new file mode 100644 index 00000000000..cb3d0b13e71 --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpMirroringFailedException.java @@ -0,0 +1,9 @@ +package org.cloudfoundry.identity.uaa.provider; + +import org.cloudfoundry.identity.uaa.error.UaaException; + +public class IdpMirroringFailedException extends UaaException { + public IdpMirroringFailedException(final String msg, final Throwable t) { + super(msg, t); + } +} diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index bfaeb647235..c5a82711fb7 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -3,6 +3,7 @@ import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.extensions.PollutionPreventionExtension; import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -15,6 +16,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.transaction.PlatformTransactionManager; import java.net.MalformedURLException; import java.net.URL; @@ -60,6 +62,12 @@ class IdentityProviderEndpointsTest { @Mock private IdentityZoneManager mockIdentityZoneManager; + @Mock + private PlatformTransactionManager mockPlatformTransactionManager; + + @Mock + private IdentityZoneProvisioning mockIdentityZoneProvisioning; + @InjectMocks private IdentityProviderEndpoints identityProviderEndpoints; From d6da7c52e98a7ea4b5b99b831c58bd428af51c41 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 18 Dec 2023 13:09:01 +0100 Subject: [PATCH 12/91] Use static import of StringUtils.hasText --- .../identity/uaa/provider/IdentityProviderEndpoints.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6459b74b139..e74c31385bd 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 @@ -268,7 +268,7 @@ public ResponseEntity deleteIdentityProvider(@PathVariable Str Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // delete mirrored IdP if alias fields are set - if (existing != null && StringUtils.hasText(existing.getAliasZid()) && StringUtils.hasText(existing.getAliasId())) { + if (existing != null && hasText(existing.getAliasZid()) && hasText(existing.getAliasId())) { IdentityProvider mirroredIdp = identityProviderProvisioning.retrieve(existing.getAliasId(), existing.getAliasZid()); mirroredIdp.setSerializeConfigRaw(rawConfig); publisher.publishEvent(new EntityDeletedEvent<>(mirroredIdp, authentication, identityZoneId)); From 0c64c0392d9bf605a1a936b516d64ed1f5c77241 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 18 Dec 2023 13:14:10 +0100 Subject: [PATCH 13/91] Add token format parameter to MockMvcUtils.getUserOAuthAccessTokenAuthCode --- .../providers/IdentityProviderEndpointsMockMvcTests.java | 4 +++- .../cloudfoundry/identity/uaa/mock/util/MockMvcUtils.java | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsMockMvcTests.java index ece75d685b7..98ffef17145 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsMockMvcTests.java @@ -22,6 +22,7 @@ import org.cloudfoundry.identity.uaa.impl.config.IdentityProviderBootstrap; import org.cloudfoundry.identity.uaa.login.Prompt; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; +import org.cloudfoundry.identity.uaa.oauth.token.TokenConstants; import org.cloudfoundry.identity.uaa.provider.*; import org.cloudfoundry.identity.uaa.provider.ldap.DynamicPasswordComparator; import org.cloudfoundry.identity.uaa.provider.saml.BootstrapSamlIdentityProviderDataTests; @@ -668,7 +669,8 @@ private String getAccessTokenForZone(final IdentityZone zone) throws Exception { adminUser.getUserName(), adminUser.getPassword(), String.join(" ", scopesForZone), - IdentityZone.getUaaZoneId() + IdentityZone.getUaaZoneId(), + TokenConstants.TokenFormat.JWT ); eventListener.clearEvents(); 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 0e400be3280..a7b274fe81d 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 @@ -35,6 +35,7 @@ import org.cloudfoundry.identity.uaa.mfa.exception.MfaAlreadyExistsException; import org.cloudfoundry.identity.uaa.oauth.client.ClientDetailsModification; import org.cloudfoundry.identity.uaa.oauth.token.TokenConstants; +import org.cloudfoundry.identity.uaa.oauth.token.TokenConstants.TokenFormat; import org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.IdentityProvider; import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; @@ -1191,6 +1192,10 @@ public static String getClientOAuthAccessToken(MockMvc mockMvc, } public static String getUserOAuthAccessTokenAuthCode(MockMvc mockMvc, String clientId, String clientSecret, String userId, String username, String password, String scope, String zoneId) throws Exception { + return getUserOAuthAccessTokenAuthCode(mockMvc, clientId, clientSecret, userId, username, password, scope, zoneId, OPAQUE); + } + + public static String getUserOAuthAccessTokenAuthCode(MockMvc mockMvc, String clientId, String clientSecret, String userId, String username, String password, String scope, String zoneId, TokenFormat tokenFormat) throws Exception { String basicDigestHeaderValue = "Basic " + new String(org.apache.commons.codec.binary.Base64.encodeBase64((clientId + ":" + clientSecret) .getBytes())); @@ -1212,7 +1217,7 @@ public static String getUserOAuthAccessTokenAuthCode(MockMvc mockMvc, String cli .session(session) .param(OAuth2Utils.GRANT_TYPE, GRANT_TYPE_AUTHORIZATION_CODE) .param(OAuth2Utils.RESPONSE_TYPE, "code") - .param(TokenConstants.REQUEST_TOKEN_FORMAT, OPAQUE.getStringValue()) + .param(TokenConstants.REQUEST_TOKEN_FORMAT, tokenFormat.getStringValue()) .param(OAuth2Utils.STATE, state) .param(OAuth2Utils.CLIENT_ID, clientId) .param(OAuth2Utils.REDIRECT_URI, "http://localhost/test"); From edf2caa617be01ec9715eb33f8980f0cda092bb4 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 18 Dec 2023 13:33:37 +0100 Subject: [PATCH 14/91] Move tests regarding alias properties of IdP endpoints to separate class --- ...ityProviderEndpointsAliasMockMvcTests.java | 583 ++++++++++++++++++ ...IdentityProviderEndpointsMockMvcTests.java | 531 ---------------- 2 files changed, 583 insertions(+), 531 deletions(-) create mode 100644 uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java new file mode 100644 index 00000000000..3a7892e192a --- /dev/null +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -0,0 +1,583 @@ +package org.cloudfoundry.identity.uaa.mock.providers; + +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +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.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + +import org.apache.commons.lang.RandomStringUtils; +import org.cloudfoundry.identity.uaa.DefaultTestContext; +import org.cloudfoundry.identity.uaa.constants.OriginKeys; +import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; +import org.cloudfoundry.identity.uaa.oauth.token.TokenConstants; +import org.cloudfoundry.identity.uaa.provider.IdentityProvider; +import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.saml.BootstrapSamlIdentityProviderDataTests; +import org.cloudfoundry.identity.uaa.scim.ScimUser; +import org.cloudfoundry.identity.uaa.test.TestClient; +import org.cloudfoundry.identity.uaa.util.JsonUtils; +import org.cloudfoundry.identity.uaa.util.UaaTokenUtils; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.util.StringUtils; + +@DefaultTestContext +class IdentityProviderEndpointsAliasMockMvcTests { + @Autowired + private MockMvc mockMvc; + + @Autowired + private TestClient testClient; + + private IdentityZone customZone; + private String adminToken; + private String identityToken; + + @BeforeEach + void setUp() throws Exception { + adminToken = testClient.getClientCredentialsOAuthAccessToken( + "admin", + "adminsecret", + ""); + identityToken = testClient.getClientCredentialsOAuthAccessToken( + "identity", + "identitysecret", + "zones.write"); + customZone = MockMvcUtils.createZoneUsingWebRequest(mockMvc, identityToken); + } + + @Nested + class Create { + @Test + void testCreate_SuccessCase_MirrorFromUaaZoneToCustomZone() throws Exception { + testCreate_SuccessCase(IdentityZone.getUaa(), customZone); + } + + @Test + void testCreate_SuccessCase_MirrorFromCustomZoneToUaaZone() throws Exception { + testCreate_SuccessCase(customZone, IdentityZone.getUaa()); + } + + @Test + void testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual_Uaa() throws Exception { + testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual(IdentityZone.getUaa()); + } + + @Test + void testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual_Custom() throws Exception { + testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual(customZone); + } + + @Test + void testCreate_ShouldReject_WhenNeitherIdzNorAliasZidIsUaa() throws Exception { + final IdentityZone otherCustomZone = MockMvcUtils.createZoneUsingWebRequest(mockMvc, identityToken); + final IdentityProvider provider = buildIdpWithAliasProperties( + customZone.getId(), + null, + otherCustomZone.getId() + ); + testCreate_ShouldReject(customZone, provider, HttpStatus.UNPROCESSABLE_ENTITY); + } + + @Test + void testCreate_ShouldReject_WhenAliasIdIsSet() throws Exception { + testCreate_ShouldReject( + customZone, + buildIdpWithAliasProperties( + customZone.getId(), + UUID.randomUUID().toString(), + IdentityZone.getUaaZoneId() + ), + HttpStatus.UNPROCESSABLE_ENTITY + ); + } + + @Test + void testCreate_ShouldReject_WhenIdzReferencedInAliasZidDoesNotExist() throws Exception { + final IdentityProvider provider = buildIdpWithAliasProperties( + IdentityZone.getUaaZoneId(), + null, + UUID.randomUUID().toString() // does not exist + ); + final IdentityZone zone = IdentityZone.getUaa(); + testCreate_ShouldReject(zone, provider, HttpStatus.UNPROCESSABLE_ENTITY); + } + + @Test + void testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone_CustomToUaa() throws Exception { + testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone( + customZone, + IdentityZone.getUaa() + ); + } + + @Test + void testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone_UaaToCustom() throws Exception { + testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone( + IdentityZone.getUaa(), + customZone + ); + } + + private void testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone( + final IdentityZone zone1, + final IdentityZone zone2 + ) throws Exception { + final String originKey = RandomStringUtils.randomAlphabetic(10); + + // create IdP with origin key in custom zone + final IdentityProvider createdIdp1 = createIdp( + zone1, + buildIdpWithAliasProperties(zone1.getId(), null, null, originKey), + getAccessTokenForZone(zone1) + ); + assertNotNull(createdIdp1); + + // then, create an IdP in the "uaa" zone with the same origin key that should be mirrored to the custom zone + testCreate_ShouldReject( + zone2, + buildIdpWithAliasProperties(zone2.getId(), null, zone1.getId(), originKey), + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + private void testCreate_SuccessCase(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + assertNotNull(zone1); + assertNotNull(zone2); + + // build IdP in zone1 with aliasZid set to zone2 + final IdentityProvider provider = buildIdpWithAliasProperties( + IdentityZone.getUaa().getId(), + null, + zone2.getId() + ); + + // create IdP in zone1 + final IdentityProvider originalIdp = createIdp(zone1, provider, getAccessTokenForZone(zone1)); + assertNotNull(originalIdp); + assertTrue(StringUtils.hasText(originalIdp.getAliasId())); + assertTrue(StringUtils.hasText(originalIdp.getAliasZid())); + assertEquals(zone2.getId(), originalIdp.getAliasZid()); + + // read mirrored IdP from zone2 + final String accessTokenZone2 = getAccessTokenForZone(zone2); + final IdentityProvider mirroredIdp = readIdpFromZone(zone2, originalIdp.getAliasId(), accessTokenZone2); + assertIdpReferencesOtherIdp(mirroredIdp, originalIdp); + assertOtherPropertiesAreEqual(originalIdp, mirroredIdp); + + // check if aliasId in first IdP is equal to the ID of the mirrored one + assertEquals(mirroredIdp.getId(), originalIdp.getAliasId()); + } + + private void testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual(final IdentityZone zone) throws Exception { + final IdentityProvider provider = buildIdpWithAliasProperties( + zone.getId(), + null, + zone.getId() + ); + testCreate_ShouldReject(zone, provider, HttpStatus.UNPROCESSABLE_ENTITY); + } + + private void testCreate_ShouldReject( + final IdentityZone zone, + final IdentityProvider idp, + final HttpStatus expectedStatus + ) throws Exception { + assertNotNull(zone); + assertNotNull(idp); + + // create IdP in zone + final MvcResult result = createIdpAndReturnResult(zone, idp, getAccessTokenForZone(zone)); + assertThat(result.getResponse().getStatus()).isEqualTo(expectedStatus.value()); + } + } + + @Nested + class Update { + @Test + void testUpdate_Success_MigrationScenario_CreateMirroredIdp_UaaToCustomZone() throws Exception { + testUpdate_MigrationScenario_ShouldCreateMirroredIdp(IdentityZone.getUaa(), customZone); + } + + @Test + void testUpdate_Success_MigrationScenario_CreateMirroredIdp_CustomToUaaZone() throws Exception { + testUpdate_MigrationScenario_ShouldCreateMirroredIdp(customZone, IdentityZone.getUaa()); + } + + @Test + void testUpdate_Success_OtherPropertiesOfAlreadyMirroredIdpAreChanged() throws Exception { + final IdentityZone zone1 = IdentityZone.getUaa(); + final IdentityZone zone2 = customZone; + + // create a mirrored IdP + final IdentityProvider originalIdp = createMirroredIdp(zone1, zone2); + + // update other property + final String newName = "new name"; + originalIdp.setName(newName); + final IdentityProvider updatedOriginalIdp = updateIdp(zone1, originalIdp, getAccessTokenForZone(zone1)); + assertNotNull(updatedOriginalIdp); + assertNotNull(updatedOriginalIdp.getAliasId()); + assertNotNull(updatedOriginalIdp.getAliasZid()); + assertEquals(zone2.getId(), updatedOriginalIdp.getAliasZid()); + + assertNotNull(updatedOriginalIdp.getName()); + assertEquals(newName, updatedOriginalIdp.getName()); + + // check if the change is propagated to the mirrored IdP + final String accessTokenZone2 = getAccessTokenForZone(zone2); + final IdentityProvider mirroredIdp = readIdpFromZone( + zone2, + updatedOriginalIdp.getAliasId(), + accessTokenZone2 + ); + assertIdpReferencesOtherIdp(mirroredIdp, updatedOriginalIdp); + assertNotNull(mirroredIdp.getName()); + assertEquals(newName, mirroredIdp.getName()); + } + + @ParameterizedTest + @MethodSource + void testUpdate_ShouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp( + final String newAliasId, + final String newAliasZid + ) throws Exception { + final IdentityProvider originalIdp = createMirroredIdp(IdentityZone.getUaa(), customZone); + originalIdp.setAliasId(newAliasId); + originalIdp.setAliasZid(newAliasZid); + updateIdp_ShouldReject(IdentityZone.getUaa(), originalIdp, HttpStatus.UNPROCESSABLE_ENTITY); + } + + private static Stream testUpdate_ShouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp() { + return Stream.of(null, "", "other").flatMap(aliasIdValue -> + Stream.of(null, "", "other").map(aliasZidValue -> + Arguments.of(aliasIdValue, aliasZidValue) + )); + } + + @Test + void testUpdate_ShouldReject_OnlyAliasIdSet_Uaa() throws Exception { + testUpdate_ShouldReject_OnlyAliasIdSet(IdentityZone.getUaa()); + } + + @Test + void testUpdate_ShouldReject_OnlyAliasIdSet_Custom() throws Exception { + testUpdate_ShouldReject_OnlyAliasIdSet(customZone); + } + + private void testUpdate_ShouldReject_OnlyAliasIdSet(final IdentityZone zone) throws Exception { + final IdentityProvider idp = buildIdpWithAliasProperties(zone.getId(), null, null); + final IdentityProvider createdProvider = createIdp(zone, idp, getAccessTokenForZone(zone)); + assertNull(createdProvider.getAliasZid()); + createdProvider.setAliasId(UUID.randomUUID().toString()); + updateIdp_ShouldReject(zone, createdProvider, HttpStatus.UNPROCESSABLE_ENTITY); + } + + @Test + void testUpdate_ShouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone() throws Exception { + final String originKey = RandomStringUtils.randomAlphabetic(10); + + final IdentityProvider existingProviderInCustomZone = buildIdpWithAliasProperties( + customZone.getId(), + null, + null, + originKey + ); + createIdp(customZone, existingProviderInCustomZone, getAccessTokenForZone(customZone)); + + final IdentityZone zone = IdentityZone.getUaa(); + final IdentityProvider idp = buildIdpWithAliasProperties( + IdentityZone.getUaa().getId(), + null, + null, + originKey // same origin key + ); + // same origin key + final IdentityProvider providerInUaaZone = createIdp(zone, idp, getAccessTokenForZone(zone)); + + providerInUaaZone.setAliasZid(customZone.getId()); + updateIdp_ShouldReject( + IdentityZone.getUaa(), + providerInUaaZone, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + @Test + void testUpdate_ShouldReject_IdpInCustomZoneMirroredToOtherCustomZone() throws Exception { + final IdentityProvider idpInCustomZone = createIdp( + customZone, + buildIdpWithAliasProperties(customZone.getId(), null, null), + getAccessTokenForZone(customZone) + ); + + // try to mirror it to another custom zone + idpInCustomZone.setAliasZid("not-uaa"); + updateIdp_ShouldReject( + customZone, + idpInCustomZone, + HttpStatus.UNPROCESSABLE_ENTITY + ); + } + + private IdentityProvider createMirroredIdp( + final IdentityZone zone1, + final IdentityZone zone2 + ) throws Exception { + final IdentityProvider provider = buildIdpWithAliasProperties( + zone1.getId(), + null, + zone2.getId() + ); + return createIdp(zone1, provider, getAccessTokenForZone(zone1)); + } + + private void testUpdate_MigrationScenario_ShouldCreateMirroredIdp( + final IdentityZone zone1, + final IdentityZone zone2 + ) throws Exception { + final String accessTokenForZone1 = getAccessTokenForZone(zone1); + + // create regular idp without alias properties in UAA zone + final IdentityProvider existingIdpWithoutAlias = createIdp( + zone1, + buildIdpWithAliasProperties(zone1.getId(), null, null), + accessTokenForZone1 + ); + assertNotNull(existingIdpWithoutAlias); + assertNotNull(existingIdpWithoutAlias.getId()); + + // perform update: set Alias ZID + existingIdpWithoutAlias.setAliasZid(zone2.getId()); + final IdentityProvider idpAfterUpdate = updateIdp( + zone1, + existingIdpWithoutAlias, + accessTokenForZone1 + ); + assertNotNull(idpAfterUpdate.getAliasId()); + assertNotNull(idpAfterUpdate.getAliasZid()); + assertEquals(zone2.getId(), idpAfterUpdate.getAliasZid()); + + // read mirrored IdP through alias id in original IdP + final String accessTokenForZone2 = getAccessTokenForZone(zone2); + final IdentityProvider mirroredIdp = readIdpFromZone( + zone2, + idpAfterUpdate.getAliasId(), + accessTokenForZone2 + ); + assertIdpReferencesOtherIdp(mirroredIdp, idpAfterUpdate); + assertOtherPropertiesAreEqual(idpAfterUpdate, mirroredIdp); + } + + private IdentityProvider updateIdp( + final IdentityZone zone, + final IdentityProvider updatePayload, + final String accessTokenForZone + ) throws Exception { + updatePayload.setIdentityZoneId(zone.getId()); + final MvcResult result = updateIdpAndReturnResult(zone, updatePayload, accessTokenForZone); + assertEquals(HttpStatus.OK.value(), result.getResponse().getStatus()); + + final IdentityProvider originalIdpAfterUpdate = JsonUtils.readValue( + result.getResponse().getContentAsString(), + IdentityProvider.class + ); + assertNotNull(originalIdpAfterUpdate); + assertNotNull(originalIdpAfterUpdate.getIdentityZoneId()); + assertEquals(zone.getId(), originalIdpAfterUpdate.getIdentityZoneId()); + return originalIdpAfterUpdate; + } + + private MvcResult updateIdpAndReturnResult( + final IdentityZone zone, + final IdentityProvider updatePayload, + final String accessTokenForZone + ) throws Exception { + final String id = updatePayload.getId(); + assertThat(id).isNotNull().isNotBlank(); + + final MockHttpServletRequestBuilder updateRequestBuilder = put("/identity-providers/" + id) + .header("Authorization", "Bearer " + accessTokenForZone) + .header(IdentityZoneSwitchingFilter.HEADER, zone.getId()) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(updatePayload)); + return mockMvc.perform(updateRequestBuilder).andReturn(); + } + + private void updateIdp_ShouldReject( + final IdentityZone zone, + final IdentityProvider idp, + final HttpStatus expectedStatusCode + ) throws Exception { + final MvcResult result = updateIdpAndReturnResult(zone, idp, getAccessTokenForZone(zone)); + assertThat(result.getResponse().getStatus()).isEqualTo(expectedStatusCode.value()); + } + } + + private void assertIdpReferencesOtherIdp(final IdentityProvider idp, final IdentityProvider referencedIdp) { + assertNotNull(idp); + assertNotNull(referencedIdp); + assertTrue(StringUtils.hasText(idp.getAliasId())); + assertEquals(referencedIdp.getId(), idp.getAliasId()); + assertTrue(StringUtils.hasText(idp.getAliasZid())); + assertEquals(referencedIdp.getIdentityZoneId(), idp.getAliasZid()); + } + + private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final IdentityProvider mirroredIdp) { + // apart from the zone ID, the configs should be identical + final SamlIdentityProviderDefinition originalIdpConfig = (SamlIdentityProviderDefinition) idp.getConfig(); + originalIdpConfig.setZoneId(null); + final SamlIdentityProviderDefinition mirroredIdpConfig = (SamlIdentityProviderDefinition) mirroredIdp.getConfig(); + mirroredIdpConfig.setZoneId(null); + assertEquals(originalIdpConfig, mirroredIdpConfig); + + // check if remaining properties are equal + assertEquals(idp.getOriginKey(), mirroredIdp.getOriginKey()); + assertEquals(idp.getName(), mirroredIdp.getName()); + assertEquals(idp.getType(), mirroredIdp.getType()); + } + + private IdentityProvider createIdp( + final IdentityZone zone, + final IdentityProvider provider, + final String accessTokenForZone + ) throws Exception { + final MvcResult createResult = createIdpAndReturnResult(zone, provider, accessTokenForZone); + assertThat(createResult.getResponse().getStatus()).isEqualTo(HttpStatus.CREATED.value()); + return JsonUtils.readValue( + createResult.getResponse().getContentAsString(), + IdentityProvider.class + ); + } + + private MvcResult createIdpAndReturnResult( + final IdentityZone zone, + final IdentityProvider idp, + final String accessTokenForZone + ) throws Exception { + final MockHttpServletRequestBuilder createRequestBuilder = post("/identity-providers") + .param("rawConfig", "true") + .header("Authorization", "Bearer " + accessTokenForZone) + .header(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER, zone.getSubdomain()) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(idp)); + return mockMvc.perform(createRequestBuilder).andReturn(); + } + + private String getAccessTokenForZone(final IdentityZone zone) throws Exception { + final List scopesForZone = getScopesForZone(zone, "admin"); + + final ScimUser adminUser = MockMvcUtils.createAdminForZone( + mockMvc, + adminToken, + String.join(",", scopesForZone), + IdentityZone.getUaaZoneId() + ); + final String accessToken = MockMvcUtils.getUserOAuthAccessTokenAuthCode( + mockMvc, + "identity", + "identitysecret", + adminUser.getId(), + adminUser.getUserName(), + adminUser.getPassword(), + String.join(" ", scopesForZone), + IdentityZone.getUaaZoneId(), + TokenConstants.TokenFormat.JWT // use JWT for later checking if all scopes are present + ); + + // check if the token contains the expected scopes + final Map claims = UaaTokenUtils.getClaims(accessToken); + assertTrue(claims.containsKey("scope")); + assertTrue(claims.get("scope") instanceof List); + final List resultingScopes = (List) claims.get("scope"); + assertThat(resultingScopes).hasSameElementsAs(scopesForZone); + + return accessToken; + } + + private IdentityProvider readIdpFromZone( + final IdentityZone zone, + final String id, + final String accessToken + ) throws Exception { + final MockHttpServletRequestBuilder getRequestBuilder = get("/identity-providers/" + id) + .param("rawConfig", "true") + .header(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER, zone.getSubdomain()) + .header("Authorization", "Bearer " + accessToken); + final MvcResult getResult = mockMvc.perform(getRequestBuilder) + .andExpect(status().isOk()) + .andReturn(); + return JsonUtils.readValue( + getResult.getResponse().getContentAsString(), + IdentityProvider.class + ); + } + + private static List getScopesForZone(final IdentityZone zone, final String... scopes) { + return Stream.of(scopes).map(scope -> String.format("zones.%s.%s", zone.getId(), scope)).collect(toList()); + } + + private static IdentityProvider buildIdpWithAliasProperties( + final String idzId, + final String aliasId, + final String aliasZid + ) { + final String originKey = RandomStringUtils.randomAlphabetic(8); + return buildIdpWithAliasProperties(idzId, aliasId, aliasZid, originKey); + } + + private static IdentityProvider buildIdpWithAliasProperties( + final String idzId, + final String aliasId, + final String aliasZid, + final String originKey + ) { + final String metadata = String.format( + BootstrapSamlIdentityProviderDataTests.xmlWithoutID, + "http://localhost:9999/metadata/" + originKey + ); + final SamlIdentityProviderDefinition samlDefinition = new SamlIdentityProviderDefinition() + .setMetaDataLocation(metadata) + .setLinkText("Test SAML Provider"); + samlDefinition.setEmailDomain(Arrays.asList("test.com", "test2.com")); + samlDefinition.setExternalGroupsWhitelist(singletonList("value")); + samlDefinition.setAttributeMappings(singletonMap("given_name", "first_name")); + + final IdentityProvider provider = new IdentityProvider<>(); + provider.setActive(true); + provider.setName(originKey); + provider.setIdentityZoneId(idzId); + provider.setType(OriginKeys.SAML); + provider.setOriginKey(originKey); + provider.setConfig(samlDefinition); + provider.setAliasId(aliasId); + provider.setAliasZid(aliasZid); + return provider; + } +} diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsMockMvcTests.java index 98ffef17145..27e2125d6ab 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsMockMvcTests.java @@ -22,7 +22,6 @@ import org.cloudfoundry.identity.uaa.impl.config.IdentityProviderBootstrap; import org.cloudfoundry.identity.uaa.login.Prompt; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; -import org.cloudfoundry.identity.uaa.oauth.token.TokenConstants; import org.cloudfoundry.identity.uaa.provider.*; import org.cloudfoundry.identity.uaa.provider.ldap.DynamicPasswordComparator; import org.cloudfoundry.identity.uaa.provider.saml.BootstrapSamlIdentityProviderDataTests; @@ -31,7 +30,6 @@ 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.util.UaaTokenUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZone; import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter; @@ -39,16 +37,11 @@ import org.cloudfoundry.identity.uaa.zone.event.IdentityProviderModifiedEvent; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.http.HttpStatus; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; import org.springframework.security.oauth2.provider.client.BaseClientDetails; @@ -62,12 +55,8 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.*; -import java.util.stream.Stream; import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; -import static java.util.stream.Collectors.toList; -import static org.assertj.core.api.Assertions.*; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LDAP; import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.USER_NAME_ATTRIBUTE_NAME; import static org.hamcrest.Matchers.containsString; @@ -225,526 +214,6 @@ void test_Create_and_Delete_SamlProvider() throws Exception { ).andExpect(status().isNotFound()); } - @Nested - class AliasTests { - private IdentityZone customZone; - - @BeforeEach - void setUp() throws Exception { - customZone = MockMvcUtils.createZoneUsingWebRequest(mockMvc, identityToken); - } - - @Nested - class Create { - @Test - void testCreate_SuccessCase_MirrorFromUaaZoneToCustomZone() throws Exception { - testCreate_SuccessCase(IdentityZone.getUaa(), customZone); - } - - @Test - void testCreate_SuccessCase_MirrorFromCustomZoneToUaaZone() throws Exception { - testCreate_SuccessCase(customZone, IdentityZone.getUaa()); - } - - @Test - void testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual_Uaa() throws Exception { - testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual(IdentityZone.getUaa()); - } - - @Test - void testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual_Custom() throws Exception { - testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual(customZone); - } - - @Test - void testCreate_ShouldReject_WhenNeitherIdzNorAliasZidIsUaa() throws Exception { - final IdentityZone otherCustomZone = MockMvcUtils.createZoneUsingWebRequest(mockMvc, identityToken); - final IdentityProvider provider = buildIdpWithAliasProperties( - customZone.getId(), - null, - otherCustomZone.getId() - ); - testCreate_ShouldReject(customZone, provider, HttpStatus.UNPROCESSABLE_ENTITY); - } - - @Test - void testCreate_ShouldReject_WhenAliasIdIsSet() throws Exception { - testCreate_ShouldReject( - customZone, - buildIdpWithAliasProperties( - customZone.getId(), - UUID.randomUUID().toString(), - IdentityZone.getUaaZoneId() - ), - HttpStatus.UNPROCESSABLE_ENTITY - ); - } - - @Test - void testCreate_ShouldReject_WhenIdzReferencedInAliasZidDoesNotExist() throws Exception { - final IdentityProvider provider = buildIdpWithAliasProperties( - IdentityZone.getUaaZoneId(), - null, - UUID.randomUUID().toString() // does not exist - ); - final IdentityZone zone = IdentityZone.getUaa(); - testCreate_ShouldReject(zone, provider, HttpStatus.UNPROCESSABLE_ENTITY); - } - - @Test - void testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone_CustomToUaa() throws Exception { - testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone( - customZone, - IdentityZone.getUaa() - ); - } - - @Test - void testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone_UaaToCustom() throws Exception { - testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone( - IdentityZone.getUaa(), - customZone - ); - } - - private void testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone( - final IdentityZone zone1, - final IdentityZone zone2 - ) throws Exception { - final String originKey = RandomStringUtils.randomAlphabetic(10); - - // create IdP with origin key in custom zone - final IdentityProvider createdIdp1 = createIdp( - zone1, - buildIdpWithAliasProperties(zone1.getId(), null, null, originKey), - getAccessTokenForZone(zone1) - ); - assertNotNull(createdIdp1); - - // then, create an IdP in the "uaa" zone with the same origin key that should be mirrored to the custom zone - testCreate_ShouldReject( - zone2, - buildIdpWithAliasProperties(zone2.getId(), null, zone1.getId(), originKey), - HttpStatus.INTERNAL_SERVER_ERROR - ); - } - - private void testCreate_SuccessCase(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - assertNotNull(zone1); - assertNotNull(zone2); - - // build IdP in zone1 with aliasZid set to zone2 - final IdentityProvider provider = buildIdpWithAliasProperties( - IdentityZone.getUaa().getId(), - null, - zone2.getId() - ); - - // create IdP in zone1 - final IdentityProvider originalIdp = createIdp(zone1, provider, getAccessTokenForZone(zone1)); - assertNotNull(originalIdp); - assertTrue(StringUtils.hasText(originalIdp.getAliasId())); - assertTrue(StringUtils.hasText(originalIdp.getAliasZid())); - assertEquals(zone2.getId(), originalIdp.getAliasZid()); - - // read mirrored IdP from zone2 - final String accessTokenZone2 = getAccessTokenForZone(zone2); - final IdentityProvider mirroredIdp = readIdpFromZone(zone2, originalIdp.getAliasId(), accessTokenZone2); - assertIdpReferencesOtherIdp(mirroredIdp, originalIdp); - assertOtherPropertiesAreEqual(originalIdp, mirroredIdp); - - // check if aliasId in first IdP is equal to the ID of the mirrored one - assertEquals(mirroredIdp.getId(), originalIdp.getAliasId()); - } - - private void testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual(final IdentityZone zone) throws Exception { - final IdentityProvider provider = buildIdpWithAliasProperties( - zone.getId(), - null, - zone.getId() - ); - testCreate_ShouldReject(zone, provider, HttpStatus.UNPROCESSABLE_ENTITY); - } - - private void testCreate_ShouldReject( - final IdentityZone zone, - final IdentityProvider idp, - final HttpStatus expectedStatus - ) throws Exception { - assertNotNull(zone); - assertNotNull(idp); - - // create IdP in zone - final MvcResult result = createIdpAndReturnResult(zone, idp, getAccessTokenForZone(zone)); - assertThat(result.getResponse().getStatus()).isEqualTo(expectedStatus.value()); - } - } - - @Nested - class Update { - @Test - void testUpdate_Success_MigrationScenario_CreateMirroredIdp_UaaToCustomZone() throws Exception { - testUpdate_MigrationScenario_ShouldCreateMirroredIdp(IdentityZone.getUaa(), customZone); - } - - @Test - void testUpdate_Success_MigrationScenario_CreateMirroredIdp_CustomToUaaZone() throws Exception { - testUpdate_MigrationScenario_ShouldCreateMirroredIdp(customZone, IdentityZone.getUaa()); - } - - @Test - void testUpdate_Success_OtherPropertiesOfAlreadyMirroredIdpAreChanged() throws Exception { - final IdentityZone zone1 = IdentityZone.getUaa(); - final IdentityZone zone2 = customZone; - - // create a mirrored IdP - final IdentityProvider originalIdp = createMirroredIdp(zone1, zone2); - - // update other property - final String newName = "new name"; - originalIdp.setName(newName); - final IdentityProvider updatedOriginalIdp = updateIdp(zone1, originalIdp, getAccessTokenForZone(zone1)); - assertNotNull(updatedOriginalIdp); - assertNotNull(updatedOriginalIdp.getAliasId()); - assertNotNull(updatedOriginalIdp.getAliasZid()); - assertEquals(zone2.getId(), updatedOriginalIdp.getAliasZid()); - - assertNotNull(updatedOriginalIdp.getName()); - assertEquals(newName, updatedOriginalIdp.getName()); - - // check if the change is propagated to the mirrored IdP - final String accessTokenZone2 = getAccessTokenForZone(zone2); - final IdentityProvider mirroredIdp = readIdpFromZone( - zone2, - updatedOriginalIdp.getAliasId(), - accessTokenZone2 - ); - assertIdpReferencesOtherIdp(mirroredIdp, updatedOriginalIdp); - assertNotNull(mirroredIdp.getName()); - assertEquals(newName, mirroredIdp.getName()); - } - - @ParameterizedTest - @MethodSource - void testUpdate_ShouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp( - final String newAliasId, - final String newAliasZid - ) throws Exception { - final IdentityProvider originalIdp = createMirroredIdp(IdentityZone.getUaa(), customZone); - originalIdp.setAliasId(newAliasId); - originalIdp.setAliasZid(newAliasZid); - updateIdp_ShouldReject(IdentityZone.getUaa(), originalIdp, HttpStatus.UNPROCESSABLE_ENTITY); - } - - private static Stream testUpdate_ShouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp() { - return Stream.of(null, "", "other").flatMap(aliasIdValue -> - Stream.of(null, "", "other").map(aliasZidValue -> - Arguments.of(aliasIdValue, aliasZidValue) - )); - } - - @Test - void testUpdate_ShouldReject_OnlyAliasIdSet_Uaa() throws Exception { - testUpdate_ShouldReject_OnlyAliasIdSet(IdentityZone.getUaa()); - } - - @Test - void testUpdate_ShouldReject_OnlyAliasIdSet_Custom() throws Exception { - testUpdate_ShouldReject_OnlyAliasIdSet(customZone); - } - - private void testUpdate_ShouldReject_OnlyAliasIdSet(final IdentityZone zone) throws Exception { - final IdentityProvider idp = buildIdpWithAliasProperties(zone.getId(), null, null); - final IdentityProvider createdProvider = createIdp(zone, idp, getAccessTokenForZone(zone)); - assertNull(createdProvider.getAliasZid()); - createdProvider.setAliasId(UUID.randomUUID().toString()); - updateIdp_ShouldReject(zone, createdProvider, HttpStatus.UNPROCESSABLE_ENTITY); - } - - @Test - void testUpdate_ShouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone() throws Exception { - final String originKey = RandomStringUtils.randomAlphabetic(10); - - final IdentityProvider existingProviderInCustomZone = buildIdpWithAliasProperties( - customZone.getId(), - null, - null, - originKey - ); - createIdp(customZone, existingProviderInCustomZone, getAccessTokenForZone(customZone)); - - final IdentityZone zone = IdentityZone.getUaa(); - final IdentityProvider idp = buildIdpWithAliasProperties( - IdentityZone.getUaa().getId(), - null, - null, - originKey // same origin key - ); - // same origin key - final IdentityProvider providerInUaaZone = createIdp(zone, idp, getAccessTokenForZone(zone)); - - providerInUaaZone.setAliasZid(customZone.getId()); - updateIdp_ShouldReject( - IdentityZone.getUaa(), - providerInUaaZone, - HttpStatus.INTERNAL_SERVER_ERROR - ); - } - - @Test - void testUpdate_ShouldReject_IdpInCustomZoneMirroredToOtherCustomZone() throws Exception { - final IdentityProvider idpInCustomZone = createIdp( - customZone, - buildIdpWithAliasProperties(customZone.getId(), null, null), - getAccessTokenForZone(customZone) - ); - - // try to mirror it to another custom zone - idpInCustomZone.setAliasZid("not-uaa"); - updateIdp_ShouldReject( - customZone, - idpInCustomZone, - HttpStatus.UNPROCESSABLE_ENTITY - ); - } - - private IdentityProvider createMirroredIdp( - final IdentityZone zone1, - final IdentityZone zone2 - ) throws Exception { - final IdentityProvider provider = buildIdpWithAliasProperties( - zone1.getId(), - null, - zone2.getId() - ); - return createIdp(zone1, provider, getAccessTokenForZone(zone1)); - } - - private void testUpdate_MigrationScenario_ShouldCreateMirroredIdp( - final IdentityZone zone1, - final IdentityZone zone2 - ) throws Exception { - final String accessTokenForZone1 = getAccessTokenForZone(zone1); - - // create regular idp without alias properties in UAA zone - final IdentityProvider existingIdpWithoutAlias = createIdp( - zone1, - buildIdpWithAliasProperties(zone1.getId(), null, null), - accessTokenForZone1 - ); - assertNotNull(existingIdpWithoutAlias); - assertNotNull(existingIdpWithoutAlias.getId()); - - // perform update: set Alias ZID - existingIdpWithoutAlias.setAliasZid(zone2.getId()); - final IdentityProvider idpAfterUpdate = updateIdp( - zone1, - existingIdpWithoutAlias, - accessTokenForZone1 - ); - assertNotNull(idpAfterUpdate.getAliasId()); - assertNotNull(idpAfterUpdate.getAliasZid()); - assertEquals(zone2.getId(), idpAfterUpdate.getAliasZid()); - - // read mirrored IdP through alias id in original IdP - final String accessTokenForZone2 = getAccessTokenForZone(zone2); - final IdentityProvider mirroredIdp = readIdpFromZone( - zone2, - idpAfterUpdate.getAliasId(), - accessTokenForZone2 - ); - assertIdpReferencesOtherIdp(mirroredIdp, idpAfterUpdate); - assertOtherPropertiesAreEqual(idpAfterUpdate, mirroredIdp); - } - - private IdentityProvider updateIdp( - final IdentityZone zone, - final IdentityProvider updatePayload, - final String accessTokenForZone - ) throws Exception { - updatePayload.setIdentityZoneId(zone.getId()); - final MvcResult result = updateIdpAndReturnResult(zone, updatePayload, accessTokenForZone); - assertEquals(HttpStatus.OK.value(), result.getResponse().getStatus()); - - final IdentityProvider originalIdpAfterUpdate = JsonUtils.readValue( - result.getResponse().getContentAsString(), - IdentityProvider.class - ); - assertNotNull(originalIdpAfterUpdate); - assertNotNull(originalIdpAfterUpdate.getIdentityZoneId()); - assertEquals(zone.getId(), originalIdpAfterUpdate.getIdentityZoneId()); - return originalIdpAfterUpdate; - } - - private MvcResult updateIdpAndReturnResult( - final IdentityZone zone, - final IdentityProvider updatePayload, - final String accessTokenForZone - ) throws Exception { - final String id = updatePayload.getId(); - assertThat(id).isNotNull().isNotBlank(); - - final MockHttpServletRequestBuilder updateRequestBuilder = put("/identity-providers/" + id) - .header("Authorization", "Bearer " + accessTokenForZone) - .header(IdentityZoneSwitchingFilter.HEADER, zone.getId()) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(updatePayload)); - return mockMvc.perform(updateRequestBuilder).andReturn(); - } - - private void updateIdp_ShouldReject( - final IdentityZone zone, - final IdentityProvider idp, - final HttpStatus expectedStatusCode - ) throws Exception { - final MvcResult result = updateIdpAndReturnResult(zone,idp, getAccessTokenForZone(zone)); - assertThat(result.getResponse().getStatus()).isEqualTo(expectedStatusCode.value()); - } - } - - private void assertIdpReferencesOtherIdp(final IdentityProvider idp, final IdentityProvider referencedIdp) { - assertNotNull(idp); - assertNotNull(referencedIdp); - assertTrue(StringUtils.hasText(idp.getAliasId())); - assertEquals(referencedIdp.getId(), idp.getAliasId()); - assertTrue(StringUtils.hasText(idp.getAliasZid())); - assertEquals(referencedIdp.getIdentityZoneId(), idp.getAliasZid()); - } - - private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final IdentityProvider mirroredIdp) { - // apart from the zone ID, the configs should be identical - final SamlIdentityProviderDefinition originalIdpConfig = (SamlIdentityProviderDefinition) idp.getConfig(); - originalIdpConfig.setZoneId(null); - final SamlIdentityProviderDefinition mirroredIdpConfig = (SamlIdentityProviderDefinition) mirroredIdp.getConfig(); - mirroredIdpConfig.setZoneId(null); - assertEquals(originalIdpConfig, mirroredIdpConfig); - - // check if remaining properties are equal - assertEquals(idp.getOriginKey(), mirroredIdp.getOriginKey()); - assertEquals(idp.getName(), mirroredIdp.getName()); - assertEquals(idp.getType(), mirroredIdp.getType()); - } - - private IdentityProvider createIdp( - final IdentityZone zone, - final IdentityProvider provider, - final String accessTokenForZone - ) throws Exception { - final MvcResult createResult = createIdpAndReturnResult(zone, provider, accessTokenForZone); - assertThat(createResult.getResponse().getStatus()).isEqualTo(HttpStatus.CREATED.value()); - return JsonUtils.readValue( - createResult.getResponse().getContentAsString(), - IdentityProvider.class - ); - } - - private MvcResult createIdpAndReturnResult( - final IdentityZone zone, - final IdentityProvider idp, - final String accessTokenForZone - ) throws Exception { - final MockHttpServletRequestBuilder createRequestBuilder = post("/identity-providers") - .param("rawConfig", "true") - .header("Authorization", "Bearer " + accessTokenForZone) - .header(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER, zone.getSubdomain()) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(idp)); - return mockMvc.perform(createRequestBuilder).andReturn(); - } - - private String getAccessTokenForZone(final IdentityZone zone) throws Exception { - final List scopesForZone = getScopesForZone(zone, "admin"); - - final ScimUser adminUser = MockMvcUtils.createAdminForZone( - mockMvc, - adminToken, - String.join(",", scopesForZone), - IdentityZone.getUaaZoneId() - ); - final String accessToken = MockMvcUtils.getUserOAuthAccessTokenAuthCode( - mockMvc, - "identity", - "identitysecret", - adminUser.getId(), - adminUser.getUserName(), - adminUser.getPassword(), - String.join(" ", scopesForZone), - IdentityZone.getUaaZoneId(), - TokenConstants.TokenFormat.JWT - ); - eventListener.clearEvents(); - - // check if the token contains the expected scopes - final Map claims = UaaTokenUtils.getClaims(accessToken); - assertTrue(claims.containsKey("scope")); - assertTrue(claims.get("scope") instanceof List); - final List resultingScopes = (List) claims.get("scope"); - assertThat(resultingScopes).hasSameElementsAs(scopesForZone); - - return accessToken; - } - - private IdentityProvider readIdpFromZone( - final IdentityZone zone, - final String id, - final String accessToken - ) throws Exception { - final MockHttpServletRequestBuilder getRequestBuilder = get("/identity-providers/" + id) - .param("rawConfig", "true") - .header(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER, zone.getSubdomain()) - .header("Authorization", "Bearer " + accessToken); - final MvcResult getResult = mockMvc.perform(getRequestBuilder) - .andExpect(status().isOk()) - .andReturn(); - return JsonUtils.readValue( - getResult.getResponse().getContentAsString(), - IdentityProvider.class - ); - } - - private static List getScopesForZone(final IdentityZone zone, final String... scopes) { - return Stream.of(scopes).map(scope -> String.format("zones.%s.%s", zone.getId(), scope)).collect(toList()); - } - - private static IdentityProvider buildIdpWithAliasProperties( - final String idzId, - final String aliasId, - final String aliasZid - ) { - final String originKey = RandomStringUtils.randomAlphabetic(8); - return buildIdpWithAliasProperties(idzId, aliasId, aliasZid, originKey); - } - - private static IdentityProvider buildIdpWithAliasProperties( - final String idzId, - final String aliasId, - final String aliasZid, - final String originKey - ) { - final String metadata = String.format( - BootstrapSamlIdentityProviderDataTests.xmlWithoutID, - "http://localhost:9999/metadata/" + originKey - ); - final SamlIdentityProviderDefinition samlDefinition = new SamlIdentityProviderDefinition() - .setMetaDataLocation(metadata) - .setLinkText("Test SAML Provider"); - samlDefinition.setEmailDomain(Arrays.asList("test.com", "test2.com")); - samlDefinition.setExternalGroupsWhitelist(singletonList("value")); - samlDefinition.setAttributeMappings(singletonMap("given_name", "first_name")); - - final IdentityProvider provider = new IdentityProvider<>(); - provider.setActive(true); - provider.setName(originKey); - provider.setIdentityZoneId(idzId); - provider.setType(OriginKeys.SAML); - provider.setOriginKey(originKey); - provider.setConfig(samlDefinition); - provider.setAliasId(aliasId); - provider.setAliasZid(aliasZid); - return provider; - } - } - @Test void test_delete_with_invalid_id_returns_404() throws Exception { String accessToken = setUpAccessToken(); From 40815531b4c08e1022ce9e5f54c1ec182ea9bea0 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 18 Dec 2023 13:35:26 +0100 Subject: [PATCH 15/91] Revert changes to IdentityProviderEndpointsMockMvcTests --- .../IdentityProviderEndpointsMockMvcTests.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsMockMvcTests.java index 27e2125d6ab..760b51417fa 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsMockMvcTests.java @@ -13,9 +13,7 @@ package org.cloudfoundry.identity.uaa.mock.providers; import com.fasterxml.jackson.core.type.TypeReference; - import org.apache.commons.lang.RandomStringUtils; -import org.assertj.core.api.Assertions; import org.cloudfoundry.identity.uaa.DefaultTestContext; import org.cloudfoundry.identity.uaa.audit.AuditEventType; import org.cloudfoundry.identity.uaa.constants.OriginKeys; @@ -56,7 +54,6 @@ import java.net.URL; import java.util.*; -import static java.util.Collections.singletonList; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LDAP; import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.USER_NAME_ATTRIBUTE_NAME; import static org.hamcrest.Matchers.containsString; @@ -122,14 +119,14 @@ void test_delete_through_event() throws Exception { IdentityProviderBootstrap bootstrap = webApplicationContext.getBean(IdentityProviderBootstrap.class); assertNotNull(identityProviderProvisioning.retrieveByOrigin(origin, IdentityZone.getUaaZoneId())); try { - bootstrap.setOriginsToDelete(singletonList(origin)); + bootstrap.setOriginsToDelete(Collections.singletonList(origin)); bootstrap.onApplicationEvent(new ContextRefreshedEvent(webApplicationContext)); } finally { bootstrap.setOriginsToDelete(null); } try { identityProviderProvisioning.retrieveByOrigin(origin, IdentityZone.getUaaZoneId()); - Assertions.fail("Identity provider should have been deleted"); + fail("Identity provider should have been deleted"); } catch (EmptyResultDataAccessException ignored) { } } @@ -237,7 +234,7 @@ void test_delete_response_not_containing_relying_party_secret() throws Exception definition.setRelyingPartySecret("secret"); definition.setShowLinkText(false); definition.setUserPropagationParameter("username"); - definition.setExternalGroupsWhitelist(singletonList("uaa.user")); + definition.setExternalGroupsWhitelist(Collections.singletonList("uaa.user")); List prompts = Arrays.asList(new Prompt("username", "text", "Email"), new Prompt("password", "password", "Password"), new Prompt("passcode", "password", "Temporary Authentication Code (Get on at /passcode)")); @@ -254,7 +251,7 @@ void test_delete_response_not_containing_relying_party_secret() throws Exception MvcResult result = mockMvc.perform(requestBuilder).andExpect(status().isOk()).andReturn(); IdentityProvider returnedIdentityProvider = JsonUtils.readValue( result.getResponse().getContentAsString(), IdentityProvider.class); - assertNull(((AbstractExternalOAuthIdentityProviderDefinition) returnedIdentityProvider.getConfig()) + assertNull(((AbstractExternalOAuthIdentityProviderDefinition)returnedIdentityProvider.getConfig()) .getRelyingPartySecret()); } @@ -323,7 +320,7 @@ void test_delete_response_not_containing_bind_password() throws Exception { IdentityProvider returnedIdentityProvider = JsonUtils.readValue( deleteResult.getResponse().getContentAsString(), IdentityProvider.class); - assertNull(((LdapIdentityProviderDefinition) returnedIdentityProvider. + assertNull(((LdapIdentityProviderDefinition)returnedIdentityProvider. getConfig()).getBindPassword()); } } From cb3788d084f35897f533c8d78d856a44efa75a67 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 18 Dec 2023 13:37:01 +0100 Subject: [PATCH 16/91] Remove comment --- .../identity/uaa/provider/IdentityProviderEndpoints.java | 1 - 1 file changed, 1 deletion(-) 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 e74c31385bd..628ad009d4b 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 @@ -191,7 +191,6 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden * alias ID and alias ZID set, the existing mirrored IdP is updated. If only the alias ZID is set, a new mirrored * IdP is created. * This method should be executed in a transaction together with the original create or update operation. - * The method assumes that * * @param originalIdp the original IdP; must be persisted, i.e., have an ID, already * @return the original IdP after the operation, with a potentially updated "aliasId" field From 8aba009f8ccd80163c4b16575bb828660bb38041 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 20 Dec 2023 11:18:42 +0100 Subject: [PATCH 17/91] Add tests for deletion operation --- ...ityProviderEndpointsAliasMockMvcTests.java | 476 +++++++++--------- 1 file changed, 234 insertions(+), 242 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 3a7892e192a..9836c2caf16 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -9,14 +9,15 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; 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; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.stream.Stream; @@ -45,7 +46,6 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; -import org.springframework.util.StringUtils; @DefaultTestContext class IdentityProviderEndpointsAliasMockMvcTests { @@ -75,80 +75,86 @@ void setUp() throws Exception { @Nested class Create { @Test - void testCreate_SuccessCase_MirrorFromUaaZoneToCustomZone() throws Exception { - testCreate_SuccessCase(IdentityZone.getUaa(), customZone); + void shouldAccept_MirrorIdp_UaaToCustomZone() throws Exception { + shouldAccept_MirrorIdp(IdentityZone.getUaa(), customZone); } @Test - void testCreate_SuccessCase_MirrorFromCustomZoneToUaaZone() throws Exception { - testCreate_SuccessCase(customZone, IdentityZone.getUaa()); + void shouldAccept_MirrorIdp_CustomToUaaZone() throws Exception { + shouldAccept_MirrorIdp(customZone, IdentityZone.getUaa()); + } + + private void shouldAccept_MirrorIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + // build IdP in zone1 with aliasZid set to zone2 + final IdentityProvider provider = buildIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); + + // create IdP in zone1 + final IdentityProvider originalIdp = createIdp(zone1, provider, getAccessTokenForZone(zone1)); + assertThat(originalIdp).isNotNull(); + assertThat(originalIdp.getAliasId()).isNotBlank(); + assertThat(originalIdp.getAliasZid()).isNotBlank().isEqualTo(zone2.getId()); + + // read mirrored IdP from zone2 + final String accessTokenZone2 = getAccessTokenForZone(zone2); + final Optional mirroredIdp = readIdpFromZoneIfExists(zone2, originalIdp.getAliasId(), accessTokenZone2); + assertThat(mirroredIdp).isPresent(); + assertIdpReferencesOtherIdp(mirroredIdp.get(), originalIdp); + assertOtherPropertiesAreEqual(originalIdp, mirroredIdp.get()); + + // check if aliasId in first IdP is equal to the ID of the mirrored one + assertThat(originalIdp.getAliasId()).isEqualTo(mirroredIdp.get().getId()); } @Test - void testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual_Uaa() throws Exception { - testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual(IdentityZone.getUaa()); + void shouldReject_IdzAndAliasZidAreEqual_UaaZone() throws Exception { + shouldReject_IdzAndAliasZidAreEqual(IdentityZone.getUaa()); } @Test - void testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual_Custom() throws Exception { - testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual(customZone); + void shouldReject_IdzAndAliasZidAreEqual_CustomZone() throws Exception { + shouldReject_IdzAndAliasZidAreEqual(customZone); + } + + private void shouldReject_IdzAndAliasZidAreEqual(final IdentityZone zone) throws Exception { + final IdentityProvider idp = buildIdpWithAliasProperties(zone.getId(), null, zone.getId()); + shouldReject(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } @Test - void testCreate_ShouldReject_WhenNeitherIdzNorAliasZidIsUaa() throws Exception { + void shouldReject_NeitherIdzNorAliasZidIsUaa() throws Exception { final IdentityZone otherCustomZone = MockMvcUtils.createZoneUsingWebRequest(mockMvc, identityToken); - final IdentityProvider provider = buildIdpWithAliasProperties( - customZone.getId(), - null, - otherCustomZone.getId() - ); - testCreate_ShouldReject(customZone, provider, HttpStatus.UNPROCESSABLE_ENTITY); + final IdentityProvider idp = buildIdpWithAliasProperties(customZone.getId(), null, otherCustomZone.getId()); + shouldReject(customZone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } @Test - void testCreate_ShouldReject_WhenAliasIdIsSet() throws Exception { - testCreate_ShouldReject( - customZone, - buildIdpWithAliasProperties( - customZone.getId(), - UUID.randomUUID().toString(), - IdentityZone.getUaaZoneId() - ), - HttpStatus.UNPROCESSABLE_ENTITY - ); + void shouldReject_AliasIdIsSet() throws Exception { + final String aliasId = UUID.randomUUID().toString(); + final IdentityProvider idp = buildIdpWithAliasProperties(customZone.getId(), aliasId, IdentityZone.getUaaZoneId()); + shouldReject(customZone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } @Test - void testCreate_ShouldReject_WhenIdzReferencedInAliasZidDoesNotExist() throws Exception { + void shouldReject_IdzReferencedInAliasZidDoesNotExist() throws Exception { final IdentityProvider provider = buildIdpWithAliasProperties( IdentityZone.getUaaZoneId(), null, UUID.randomUUID().toString() // does not exist ); - final IdentityZone zone = IdentityZone.getUaa(); - testCreate_ShouldReject(zone, provider, HttpStatus.UNPROCESSABLE_ENTITY); + shouldReject(IdentityZone.getUaa(), provider, HttpStatus.UNPROCESSABLE_ENTITY); } @Test - void testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone_CustomToUaa() throws Exception { - testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone( - customZone, - IdentityZone.getUaa() - ); + void shouldReject_IdpWithOriginAlreadyExistsInAliasZone_CustomToUaaZone() throws Exception { + shouldReject_IdpWithOriginAlreadyExistsInAliasZone(customZone, IdentityZone.getUaa()); } @Test - void testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone_UaaToCustom() throws Exception { - testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone( - IdentityZone.getUaa(), - customZone - ); + void shouldReject_IdpWithOriginAlreadyExistsInAliasZone_UaaToCustomZone() throws Exception { + shouldReject_IdpWithOriginAlreadyExistsInAliasZone(IdentityZone.getUaa(), customZone); } - private void testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone( - final IdentityZone zone1, - final IdentityZone zone2 - ) throws Exception { + private void shouldReject_IdpWithOriginAlreadyExistsInAliasZone(final IdentityZone zone1, final IdentityZone zone2) throws Exception { final String originKey = RandomStringUtils.randomAlphabetic(10); // create IdP with origin key in custom zone @@ -160,59 +166,14 @@ private void testCreate_ShouldReject_IdpWithOriginAlreadyExistsInAliasZone( assertNotNull(createdIdp1); // then, create an IdP in the "uaa" zone with the same origin key that should be mirrored to the custom zone - testCreate_ShouldReject( + shouldReject( zone2, buildIdpWithAliasProperties(zone2.getId(), null, zone1.getId(), originKey), HttpStatus.INTERNAL_SERVER_ERROR ); } - private void testCreate_SuccessCase(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - assertNotNull(zone1); - assertNotNull(zone2); - - // build IdP in zone1 with aliasZid set to zone2 - final IdentityProvider provider = buildIdpWithAliasProperties( - IdentityZone.getUaa().getId(), - null, - zone2.getId() - ); - - // create IdP in zone1 - final IdentityProvider originalIdp = createIdp(zone1, provider, getAccessTokenForZone(zone1)); - assertNotNull(originalIdp); - assertTrue(StringUtils.hasText(originalIdp.getAliasId())); - assertTrue(StringUtils.hasText(originalIdp.getAliasZid())); - assertEquals(zone2.getId(), originalIdp.getAliasZid()); - - // read mirrored IdP from zone2 - final String accessTokenZone2 = getAccessTokenForZone(zone2); - final IdentityProvider mirroredIdp = readIdpFromZone(zone2, originalIdp.getAliasId(), accessTokenZone2); - assertIdpReferencesOtherIdp(mirroredIdp, originalIdp); - assertOtherPropertiesAreEqual(originalIdp, mirroredIdp); - - // check if aliasId in first IdP is equal to the ID of the mirrored one - assertEquals(mirroredIdp.getId(), originalIdp.getAliasId()); - } - - private void testCreate_ShouldReject_WhenIdzAndAliasZidAreEqual(final IdentityZone zone) throws Exception { - final IdentityProvider provider = buildIdpWithAliasProperties( - zone.getId(), - null, - zone.getId() - ); - testCreate_ShouldReject(zone, provider, HttpStatus.UNPROCESSABLE_ENTITY); - } - - private void testCreate_ShouldReject( - final IdentityZone zone, - final IdentityProvider idp, - final HttpStatus expectedStatus - ) throws Exception { - assertNotNull(zone); - assertNotNull(idp); - - // create IdP in zone + private void shouldReject(final IdentityZone zone, final IdentityProvider idp, final HttpStatus expectedStatus) throws Exception { final MvcResult result = createIdpAndReturnResult(zone, idp, getAccessTokenForZone(zone)); assertThat(result.getResponse().getStatus()).isEqualTo(expectedStatus.value()); } @@ -221,20 +182,55 @@ private void testCreate_ShouldReject( @Nested class Update { @Test - void testUpdate_Success_MigrationScenario_CreateMirroredIdp_UaaToCustomZone() throws Exception { - testUpdate_MigrationScenario_ShouldCreateMirroredIdp(IdentityZone.getUaa(), customZone); + void shouldAccept_ShouldCreateMirroredIdp_UaaToCustomZone() throws Exception { + shouldAccept_ShouldCreateMirroredIdp(IdentityZone.getUaa(), customZone); + } + + @Test + void shouldAccept_ShouldCreateMirroredIdp_CustomToUaaZone() throws Exception { + shouldAccept_ShouldCreateMirroredIdp(customZone, IdentityZone.getUaa()); + } + + private void shouldAccept_ShouldCreateMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + final String accessTokenForZone1 = getAccessTokenForZone(zone1); + + // create regular idp without alias properties in UAA zone + final IdentityProvider existingIdpWithoutAlias = createIdp( + zone1, + buildIdpWithAliasProperties(zone1.getId(), null, null), + accessTokenForZone1 + ); + assertNotNull(existingIdpWithoutAlias); + assertNotNull(existingIdpWithoutAlias.getId()); + + // perform update: set Alias ZID + existingIdpWithoutAlias.setAliasZid(zone2.getId()); + final IdentityProvider idpAfterUpdate = updateIdp(zone1, existingIdpWithoutAlias, accessTokenForZone1); + assertNotNull(idpAfterUpdate.getAliasId()); + assertNotNull(idpAfterUpdate.getAliasZid()); + assertEquals(zone2.getId(), idpAfterUpdate.getAliasZid()); + + // read mirrored IdP through alias id in original IdP + final String accessTokenForZone2 = getAccessTokenForZone(zone2); + final String id = idpAfterUpdate.getAliasId(); + final Optional idp = readIdpFromZoneIfExists(zone2, id, accessTokenForZone2); + assertThat(idp).isPresent(); + final IdentityProvider mirroredIdp = idp.get(); + assertIdpReferencesOtherIdp(mirroredIdp, idpAfterUpdate); + assertOtherPropertiesAreEqual(idpAfterUpdate, mirroredIdp); } @Test - void testUpdate_Success_MigrationScenario_CreateMirroredIdp_CustomToUaaZone() throws Exception { - testUpdate_MigrationScenario_ShouldCreateMirroredIdp(customZone, IdentityZone.getUaa()); + void shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged_UaaToCustomZone() throws Exception { + shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged(IdentityZone.getUaa(), customZone); } @Test - void testUpdate_Success_OtherPropertiesOfAlreadyMirroredIdpAreChanged() throws Exception { - final IdentityZone zone1 = IdentityZone.getUaa(); - final IdentityZone zone2 = customZone; + void shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged_CustomToUaaZone() throws Exception { + shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged(customZone, IdentityZone.getUaa()); + } + private void shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged(final IdentityZone zone1, final IdentityZone zone2) throws Exception { // create a mirrored IdP final IdentityProvider originalIdp = createMirroredIdp(zone1, zone2); @@ -242,39 +238,46 @@ void testUpdate_Success_OtherPropertiesOfAlreadyMirroredIdpAreChanged() throws E final String newName = "new name"; originalIdp.setName(newName); final IdentityProvider updatedOriginalIdp = updateIdp(zone1, originalIdp, getAccessTokenForZone(zone1)); - assertNotNull(updatedOriginalIdp); - assertNotNull(updatedOriginalIdp.getAliasId()); - assertNotNull(updatedOriginalIdp.getAliasZid()); - assertEquals(zone2.getId(), updatedOriginalIdp.getAliasZid()); - - assertNotNull(updatedOriginalIdp.getName()); - assertEquals(newName, updatedOriginalIdp.getName()); + assertThat(updatedOriginalIdp).isNotNull(); + assertThat(updatedOriginalIdp.getAliasId()).isNotBlank(); + assertThat(updatedOriginalIdp.getAliasZid()).isNotBlank(); + assertThat(updatedOriginalIdp.getAliasZid()).isEqualTo(zone2.getId()); + assertThat(updatedOriginalIdp.getName()).isNotBlank().isEqualTo(newName); // check if the change is propagated to the mirrored IdP final String accessTokenZone2 = getAccessTokenForZone(zone2); - final IdentityProvider mirroredIdp = readIdpFromZone( - zone2, - updatedOriginalIdp.getAliasId(), - accessTokenZone2 - ); - assertIdpReferencesOtherIdp(mirroredIdp, updatedOriginalIdp); - assertNotNull(mirroredIdp.getName()); - assertEquals(newName, mirroredIdp.getName()); + final String id = updatedOriginalIdp.getAliasId(); + final Optional mirroredIdp = readIdpFromZoneIfExists(zone2, id, accessTokenZone2); + assertThat(mirroredIdp).isPresent(); + assertIdpReferencesOtherIdp(mirroredIdp.get(), updatedOriginalIdp); + assertThat(mirroredIdp.get().getName()).isNotBlank().isEqualTo(newName); + } + + @ParameterizedTest + @MethodSource("shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp") + void shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp_UaaToCustomZone(final String newAliasId, final String newAliasZid) throws Exception { + shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp(newAliasId, newAliasZid, IdentityZone.getUaa(), customZone); } @ParameterizedTest - @MethodSource - void testUpdate_ShouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp( + @MethodSource("shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp") + void shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp_CustomToUaaZone(final String newAliasId, final String newAliasZid) throws Exception { + shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp(newAliasId, newAliasZid, customZone, IdentityZone.getUaa()); + } + + private void shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp( final String newAliasId, - final String newAliasZid + final String newAliasZid, + final IdentityZone zone1, + final IdentityZone zone2 ) throws Exception { - final IdentityProvider originalIdp = createMirroredIdp(IdentityZone.getUaa(), customZone); + final IdentityProvider originalIdp = createMirroredIdp(zone1, zone2); originalIdp.setAliasId(newAliasId); originalIdp.setAliasZid(newAliasZid); - updateIdp_ShouldReject(IdentityZone.getUaa(), originalIdp, HttpStatus.UNPROCESSABLE_ENTITY); + shouldReject(zone1, originalIdp, HttpStatus.UNPROCESSABLE_ENTITY); } - private static Stream testUpdate_ShouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp() { + private static Stream shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp() { return Stream.of(null, "", "other").flatMap(aliasIdValue -> Stream.of(null, "", "other").map(aliasZidValue -> Arguments.of(aliasIdValue, aliasZidValue) @@ -282,55 +285,61 @@ private static Stream testUpdate_ShouldReject_ChangingAliasProperties } @Test - void testUpdate_ShouldReject_OnlyAliasIdSet_Uaa() throws Exception { - testUpdate_ShouldReject_OnlyAliasIdSet(IdentityZone.getUaa()); + void shouldReject_OnlyAliasIdSet_UaaZone() throws Exception { + shouldReject_OnlyAliasIdSet(IdentityZone.getUaa()); } @Test - void testUpdate_ShouldReject_OnlyAliasIdSet_Custom() throws Exception { - testUpdate_ShouldReject_OnlyAliasIdSet(customZone); + void shouldReject_OnlyAliasIdSet_CustomZone() throws Exception { + shouldReject_OnlyAliasIdSet(customZone); } - private void testUpdate_ShouldReject_OnlyAliasIdSet(final IdentityZone zone) throws Exception { + private void shouldReject_OnlyAliasIdSet(final IdentityZone zone) throws Exception { final IdentityProvider idp = buildIdpWithAliasProperties(zone.getId(), null, null); final IdentityProvider createdProvider = createIdp(zone, idp, getAccessTokenForZone(zone)); assertNull(createdProvider.getAliasZid()); createdProvider.setAliasId(UUID.randomUUID().toString()); - updateIdp_ShouldReject(zone, createdProvider, HttpStatus.UNPROCESSABLE_ENTITY); + shouldReject(zone, createdProvider, HttpStatus.UNPROCESSABLE_ENTITY); } @Test - void testUpdate_ShouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone() throws Exception { + void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone_UaaToCustomZone() throws Exception { + shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(IdentityZone.getUaa(), customZone); + } + + @Test + void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone_CustomToUaaZone() throws Exception { + shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(customZone, IdentityZone.getUaa()); + } + + private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final IdentityZone zone1, final IdentityZone zone2) throws Exception { final String originKey = RandomStringUtils.randomAlphabetic(10); - final IdentityProvider existingProviderInCustomZone = buildIdpWithAliasProperties( - customZone.getId(), + // create IdP with origin key in zone 2 + final IdentityProvider existingIdpInZone2 = buildIdpWithAliasProperties( + zone2.getId(), null, null, originKey ); - createIdp(customZone, existingProviderInCustomZone, getAccessTokenForZone(customZone)); + createIdp(zone2, existingIdpInZone2, getAccessTokenForZone(zone2)); - final IdentityZone zone = IdentityZone.getUaa(); + // create IdP with same origin key in zone 1 final IdentityProvider idp = buildIdpWithAliasProperties( - IdentityZone.getUaa().getId(), + zone1.getId(), null, null, originKey // same origin key ); - // same origin key - final IdentityProvider providerInUaaZone = createIdp(zone, idp, getAccessTokenForZone(zone)); + final IdentityProvider providerInZone1 = createIdp(zone1, idp, getAccessTokenForZone(zone1)); - providerInUaaZone.setAliasZid(customZone.getId()); - updateIdp_ShouldReject( - IdentityZone.getUaa(), - providerInUaaZone, - HttpStatus.INTERNAL_SERVER_ERROR - ); + // update the alias ZID to zone 2, where an IdP with this origin already exists -> should fail + providerInZone1.setAliasZid(zone2.getId()); + shouldReject(zone1, providerInZone1, HttpStatus.INTERNAL_SERVER_ERROR); } @Test - void testUpdate_ShouldReject_IdpInCustomZoneMirroredToOtherCustomZone() throws Exception { + void shouldReject_IdpInCustomZoneMirroredToOtherCustomZone() throws Exception { final IdentityProvider idpInCustomZone = createIdp( customZone, buildIdpWithAliasProperties(customZone.getId(), null, null), @@ -339,60 +348,7 @@ void testUpdate_ShouldReject_IdpInCustomZoneMirroredToOtherCustomZone() throws E // try to mirror it to another custom zone idpInCustomZone.setAliasZid("not-uaa"); - updateIdp_ShouldReject( - customZone, - idpInCustomZone, - HttpStatus.UNPROCESSABLE_ENTITY - ); - } - - private IdentityProvider createMirroredIdp( - final IdentityZone zone1, - final IdentityZone zone2 - ) throws Exception { - final IdentityProvider provider = buildIdpWithAliasProperties( - zone1.getId(), - null, - zone2.getId() - ); - return createIdp(zone1, provider, getAccessTokenForZone(zone1)); - } - - private void testUpdate_MigrationScenario_ShouldCreateMirroredIdp( - final IdentityZone zone1, - final IdentityZone zone2 - ) throws Exception { - final String accessTokenForZone1 = getAccessTokenForZone(zone1); - - // create regular idp without alias properties in UAA zone - final IdentityProvider existingIdpWithoutAlias = createIdp( - zone1, - buildIdpWithAliasProperties(zone1.getId(), null, null), - accessTokenForZone1 - ); - assertNotNull(existingIdpWithoutAlias); - assertNotNull(existingIdpWithoutAlias.getId()); - - // perform update: set Alias ZID - existingIdpWithoutAlias.setAliasZid(zone2.getId()); - final IdentityProvider idpAfterUpdate = updateIdp( - zone1, - existingIdpWithoutAlias, - accessTokenForZone1 - ); - assertNotNull(idpAfterUpdate.getAliasId()); - assertNotNull(idpAfterUpdate.getAliasZid()); - assertEquals(zone2.getId(), idpAfterUpdate.getAliasZid()); - - // read mirrored IdP through alias id in original IdP - final String accessTokenForZone2 = getAccessTokenForZone(zone2); - final IdentityProvider mirroredIdp = readIdpFromZone( - zone2, - idpAfterUpdate.getAliasId(), - accessTokenForZone2 - ); - assertIdpReferencesOtherIdp(mirroredIdp, idpAfterUpdate); - assertOtherPropertiesAreEqual(idpAfterUpdate, mirroredIdp); + shouldReject(customZone, idpInCustomZone, HttpStatus.UNPROCESSABLE_ENTITY); } private IdentityProvider updateIdp( @@ -430,23 +386,64 @@ private MvcResult updateIdpAndReturnResult( return mockMvc.perform(updateRequestBuilder).andReturn(); } - private void updateIdp_ShouldReject( - final IdentityZone zone, - final IdentityProvider idp, - final HttpStatus expectedStatusCode - ) throws Exception { + private void shouldReject(final IdentityZone zone, final IdentityProvider idp, final HttpStatus expectedStatus) throws Exception { final MvcResult result = updateIdpAndReturnResult(zone, idp, getAccessTokenForZone(zone)); - assertThat(result.getResponse().getStatus()).isEqualTo(expectedStatusCode.value()); + assertThat(result.getResponse().getStatus()).isEqualTo(expectedStatus.value()); + } + } + + @Nested + class Delete { + @Test + void shouldDeleteMirroredIdp_UaaToCustomZone() throws Exception { + shouldDeleteMirroredIdp(IdentityZone.getUaa(), customZone); + } + + @Test + void shouldDeleteMirroredIdp_CustomToUaaZone() throws Exception { + shouldDeleteMirroredIdp(customZone, IdentityZone.getUaa()); + } + + private void shouldDeleteMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + final IdentityProvider idpInZone1 = createMirroredIdp(zone1, zone2); + final String id = idpInZone1.getId(); + assertThat(id).isNotBlank(); + final String aliasId = idpInZone1.getAliasId(); + assertThat(aliasId).isNotBlank(); + final String aliasZid = idpInZone1.getAliasZid(); + assertThat(aliasZid).isNotBlank().isEqualTo(zone2.getId()); + + // check if mirrored IdP is available in zone 2 + final String accessTokenForZone2 = getAccessTokenForZone(zone2); + final Optional mirroredIdp = readIdpFromZoneIfExists(zone2, aliasId, accessTokenForZone2); + assertThat(mirroredIdp).isPresent(); + assertThat(mirroredIdp.get().getAliasId()).isNotBlank().isEqualTo(id); + assertThat(mirroredIdp.get().getAliasZid()).isNotBlank().isEqualTo(idpInZone1.getIdentityZoneId()); + + // delete IdP in zone 1 + final String accessTokenForZone1 = getAccessTokenForZone(zone1); + final MockHttpServletRequestBuilder deleteRequestBuilder = delete("/identity-providers/" + id) + .header("Authorization", "Bearer " + accessTokenForZone1) + .header(IdentityZoneSwitchingFilter.HEADER, zone1.getId()); + final var response = mockMvc.perform(deleteRequestBuilder).andReturn(); + + assertThat(response.getResponse().getStatus()).isEqualTo(200); + + // check if IdP is no longer available in zone 2 + final Optional mirroredIdpAfterDeletionOfOriginalIdp = readIdpFromZoneIfExists( + zone2, + aliasId, + accessTokenForZone2 + ); + assertThat(mirroredIdpAfterDeletionOfOriginalIdp).isNotPresent(); } } private void assertIdpReferencesOtherIdp(final IdentityProvider idp, final IdentityProvider referencedIdp) { - assertNotNull(idp); - assertNotNull(referencedIdp); - assertTrue(StringUtils.hasText(idp.getAliasId())); - assertEquals(referencedIdp.getId(), idp.getAliasId()); - assertTrue(StringUtils.hasText(idp.getAliasZid())); - assertEquals(referencedIdp.getIdentityZoneId(), idp.getAliasZid()); + assertThat(idp).isNotNull(); + assertThat(referencedIdp).isNotNull(); + assertThat(referencedIdp.getId()).isNotBlank().isEqualTo(idp.getAliasId()); + assertThat(referencedIdp.getIdentityZoneId()).isNotBlank().isEqualTo(idp.getAliasZid()); } private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final IdentityProvider mirroredIdp) { @@ -463,24 +460,18 @@ private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final Ide assertEquals(idp.getType(), mirroredIdp.getType()); } - private IdentityProvider createIdp( - final IdentityZone zone, - final IdentityProvider provider, - final String accessTokenForZone - ) throws Exception { - final MvcResult createResult = createIdpAndReturnResult(zone, provider, accessTokenForZone); + private IdentityProvider createMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + final IdentityProvider provider = buildIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); + return createIdp(zone1, provider, getAccessTokenForZone(zone1)); + } + + private IdentityProvider createIdp(final IdentityZone zone, final IdentityProvider idp, final String accessTokenForZone) throws Exception { + final MvcResult createResult = createIdpAndReturnResult(zone, idp, accessTokenForZone); assertThat(createResult.getResponse().getStatus()).isEqualTo(HttpStatus.CREATED.value()); - return JsonUtils.readValue( - createResult.getResponse().getContentAsString(), - IdentityProvider.class - ); + return JsonUtils.readValue(createResult.getResponse().getContentAsString(), IdentityProvider.class); } - private MvcResult createIdpAndReturnResult( - final IdentityZone zone, - final IdentityProvider idp, - final String accessTokenForZone - ) throws Exception { + private MvcResult createIdpAndReturnResult(final IdentityZone zone, final IdentityProvider idp, final String accessTokenForZone) throws Exception { final MockHttpServletRequestBuilder createRequestBuilder = post("/identity-providers") .param("rawConfig", "true") .header("Authorization", "Bearer " + accessTokenForZone) @@ -521,43 +512,44 @@ private String getAccessTokenForZone(final IdentityZone zone) throws Exception { return accessToken; } - private IdentityProvider readIdpFromZone( + private Optional readIdpFromZoneIfExists( final IdentityZone zone, final String id, - final String accessToken + final String accessTokenForZone ) throws Exception { final MockHttpServletRequestBuilder getRequestBuilder = get("/identity-providers/" + id) .param("rawConfig", "true") .header(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER, zone.getSubdomain()) - .header("Authorization", "Bearer " + accessToken); - final MvcResult getResult = mockMvc.perform(getRequestBuilder) - .andExpect(status().isOk()) - .andReturn(); - return JsonUtils.readValue( - getResult.getResponse().getContentAsString(), - IdentityProvider.class - ); + .header("Authorization", "Bearer " + accessTokenForZone); + final MvcResult getResult = mockMvc.perform(getRequestBuilder).andReturn(); + final int responseStatus = getResult.getResponse().getStatus(); + assertThat(responseStatus).isIn(404, 200); + + switch (responseStatus) { + case 404: + return Optional.empty(); + case 200: + final IdentityProvider responseBody = JsonUtils.readValue( + getResult.getResponse().getContentAsString(), + IdentityProvider.class + ); + return Optional.of(responseBody); + default: + // should not happen + return Optional.empty(); + } } private static List getScopesForZone(final IdentityZone zone, final String... scopes) { return Stream.of(scopes).map(scope -> String.format("zones.%s.%s", zone.getId(), scope)).collect(toList()); } - private static IdentityProvider buildIdpWithAliasProperties( - final String idzId, - final String aliasId, - final String aliasZid - ) { + private static IdentityProvider buildIdpWithAliasProperties(final String idzId, final String aliasId, final String aliasZid) { final String originKey = RandomStringUtils.randomAlphabetic(8); return buildIdpWithAliasProperties(idzId, aliasId, aliasZid, originKey); } - private static IdentityProvider buildIdpWithAliasProperties( - final String idzId, - final String aliasId, - final String aliasZid, - final String originKey - ) { + private static IdentityProvider buildIdpWithAliasProperties(final String idzId, final String aliasId, final String aliasZid, final String originKey) { final String metadata = String.format( BootstrapSamlIdentityProviderDataTests.xmlWithoutID, "http://localhost:9999/metadata/" + originKey From 4dd7c955de3b6cd41622cb00c08fa09719116627 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 20 Dec 2023 11:47:06 +0100 Subject: [PATCH 18/91] Consider mirrored IdPs in IdP status update endpoint --- .../provider/IdentityProviderEndpoints.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 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 628ad009d4b..e0467090520 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 @@ -379,8 +379,31 @@ public ResponseEntity updateIdentityProviderStatus(@Path logger.debug("IDP does not have an existing PasswordPolicy. Operation not supported"); return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } - uaaIdentityProviderDefinition.getPasswordPolicy().setPasswordNewerThan(new Date(System.currentTimeMillis())); - identityProviderProvisioning.update(existing, zoneId); + + final Date passwordNewerThanTimestamp = new Date(System.currentTimeMillis()); + uaaIdentityProviderDefinition.getPasswordPolicy().setPasswordNewerThan(passwordNewerThanTimestamp); + + // update the property in the mirrored IdP if present + final IdentityProvider mirroredIdp; + if (hasText(existing.getAliasZid()) && hasText(existing.getAliasId())) { + mirroredIdp = identityProviderProvisioning.retrieve(existing.getAliasId(), existing.getAliasZid()); + final UaaIdentityProviderDefinition definitionMirroredIdp = ObjectUtils.castInstance( + mirroredIdp.getConfig(), + UaaIdentityProviderDefinition.class + ); + definitionMirroredIdp.getPasswordPolicy().setPasswordNewerThan(passwordNewerThanTimestamp); + } else { + mirroredIdp = null; + } + + // update both IdPs in a transaction + transactionTemplate.executeWithoutResult(txStatus -> { + identityProviderProvisioning.update(existing, zoneId); + if (mirroredIdp != null) { + identityProviderProvisioning.update(mirroredIdp, mirroredIdp.getIdentityZoneId()); + } + }); + logger.info("PasswordChangeRequired property set for Identity Provider: " + existing.getId()); return new ResponseEntity<>(body, OK); } From 12bd9a7eb42351826f187082745d86798f309925 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 20 Dec 2023 14:03:12 +0100 Subject: [PATCH 19/91] Fix unit tests --- .../uaa/provider/IdentityProviderEndpoints.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 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 e0467090520..199f6e774a4 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 @@ -40,10 +40,8 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionException; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; -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; @@ -174,11 +172,9 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden return ensureConsistencyOfMirroredIdp(createdOriginalIdp); }); - } catch (final TransactionException e) { - if (e.getCause() instanceof IdpAlreadyExistsException) { - return new ResponseEntity<>(body, CONFLICT); - } - + } catch (final IdpAlreadyExistsException e) { + return new ResponseEntity<>(body, CONFLICT); + } catch (final Exception e) { logger.error("Unable to create IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "]", e); return new ResponseEntity<>(body, INTERNAL_SERVER_ERROR); } @@ -187,9 +183,9 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden } /** - * Ensure consistency with a mirrored IdP referenced in the original IdPs alias properties. If the IdP has both its - * alias ID and alias ZID set, the existing mirrored IdP is updated. If only the alias ZID is set, a new mirrored - * IdP is created. + * Ensure consistency during create or update operations with a mirrored IdP referenced in the original IdPs alias + * properties. If the IdP has both its alias ID and alias ZID set, the existing mirrored IdP is updated. If only + * the alias ZID is set, a new mirrored IdP is created. * This method should be executed in a transaction together with the original create or update operation. * * @param originalIdp the original IdP; must be persisted, i.e., have an ID, already From 86c2431d1d17f2fe982a8d83ee4afd0ba34bf688 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 20 Dec 2023 14:04:16 +0100 Subject: [PATCH 20/91] Add creation of new mirrored IdP if aliasId is set, but the referenced IdP cannot be found --- .../provider/IdentityProviderEndpoints.java | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 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 199f6e774a4..0cfd3c1f210 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 @@ -213,20 +213,17 @@ private IdentityProvider ensureConsistencyOfMirroredIdp(final IdentityProvider o .setIdentityZoneId(originalIdp.getAliasZid()); mirroredIdp.setSerializeConfigRaw(originalIdp.isSerializeConfigRaw()); + // get the referenced, mirrored IdP + final IdentityProvider existingMirroredIdp; if (hasText(originalIdp.getAliasId())) { - // retrieve and update existing mirrored IdP - final IdentityProvider existingMirroredIdp; - try { - existingMirroredIdp = identityProviderProvisioning.retrieve( - originalIdp.getAliasId(), - originalIdp.getAliasZid() - ); - } catch (final EmptyResultDataAccessException e) { - throw new IdpMirroringFailedException(String.format( - "The IdP referenced in the 'aliasId' and 'aliasZid' properties of IdP '%s' does not exist.", - originalIdp.getId() - ), e); - } + // if the referenced IdP does not exist, we create a new one + existingMirroredIdp = retrieveMirroredIdp(originalIdp); + } else { + existingMirroredIdp = null; + } + + // update the existing mirrored IdP + if (existingMirroredIdp != null) { mirroredIdp.setId(existingMirroredIdp.getId()); identityProviderProvisioning.update(mirroredIdp, originalIdp.getAliasZid()); return originalIdp; @@ -254,6 +251,23 @@ private IdentityProvider ensureConsistencyOfMirroredIdp(final IdentityProvider o return identityProviderProvisioning.update(originalIdp, originalIdp.getIdentityZoneId()); } + private IdentityProvider retrieveMirroredIdp(final IdentityProvider originalIdp) { + try { + return identityProviderProvisioning.retrieve( + originalIdp.getAliasId(), + originalIdp.getAliasZid() + ); + } catch (final EmptyResultDataAccessException e) { + logger.warn( + "The IdP referenced in the 'aliasId' ('{}') and 'aliasZid' ('{}') of the IdP '{}' does not exist.", + originalIdp.getAliasId(), + originalIdp.getAliasZid(), + originalIdp.getId() + ); + return null; + } + } + @RequestMapping(value = "{id}", method = DELETE) @Transactional public ResponseEntity deleteIdentityProvider(@PathVariable String id, @RequestParam(required = false, defaultValue = "false") boolean rawConfig) { From 9ce3ebe86b4826b4752a8aba68795f97748d9fd0 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 20 Dec 2023 14:19:15 +0100 Subject: [PATCH 21/91] Fix IdentityProviderEndpointsAliasMockMvcTests --- .../providers/IdentityProviderEndpointsAliasMockMvcTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 9836c2caf16..7df7d12c7ac 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -169,7 +169,7 @@ private void shouldReject_IdpWithOriginAlreadyExistsInAliasZone(final IdentityZo shouldReject( zone2, buildIdpWithAliasProperties(zone2.getId(), null, zone1.getId(), originKey), - HttpStatus.INTERNAL_SERVER_ERROR + HttpStatus.CONFLICT ); } From 94cd995677f7552e166ccfabb03bf4c53fb1d5b2 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 20 Dec 2023 15:31:53 +0100 Subject: [PATCH 22/91] Simplify validation of alias properties --- .../provider/IdentityProviderEndpoints.java | 293 +++++++++--------- ...ityProviderEndpointsAliasMockMvcTests.java | 20 ++ 2 files changed, 161 insertions(+), 152 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 0cfd3c1f210..274af91ec0e 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 @@ -34,6 +34,8 @@ import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.core.Authentication; @@ -134,34 +136,10 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden body.setConfig(definition); } - // at this point, the alias ID must not be set - if (hasText(body.getAliasId())) { - logger.debug("IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "] - Alias ID was not null."); + if (!aliasPropertiesAreValid(body, null)) { return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } - if (hasText(body.getAliasZid())) { - // check if the zone exists - try { - identityZoneProvisioning.retrieve(body.getAliasZid()); - } catch (final ZoneDoesNotExistsException e) { - logger.debug("IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "] - Zone referenced in alias zone ID does not exist."); - return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); - } - - // mirroring is only allowed from or to the "uaa" zone - if (!zoneId.equals(UAA) && !body.getAliasZid().equals(UAA)) { - logger.debug("IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "] - Invalid: Alias ZID set to custom zone, IdP created in custom zone."); - return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); - } - - // mirroring cannot be done to the same zone - if (body.getAliasZid().equals(zoneId)) { - logger.debug("IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "] - Invalid: Alias ZID equal to current IdZ."); - return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); - } - } - // persist IdP and mirror if necessary final IdentityProvider createdIdp; try { @@ -182,92 +160,6 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden return new ResponseEntity<>(createdIdp, CREATED); } - /** - * Ensure consistency during create or update operations with a mirrored IdP referenced in the original IdPs alias - * properties. If the IdP has both its alias ID and alias ZID set, the existing mirrored IdP is updated. If only - * the alias ZID is set, a new mirrored IdP is created. - * This method should be executed in a transaction together with the original create or update operation. - * - * @param originalIdp the original IdP; must be persisted, i.e., have an ID, already - * @return the original IdP after the operation, with a potentially updated "aliasId" field - * @throws IdpMirroringFailedException if a new mirrored IdP needs to be created, but the zone referenced in - * 'aliasZid' does not exist - * @throws IdpMirroringFailedException if 'aliasId' and 'aliasZid' are set in the original IdP, but the referenced - * mirrored IdP could not be found - */ - private IdentityProvider ensureConsistencyOfMirroredIdp(final IdentityProvider originalIdp) throws IdpMirroringFailedException { - if (!hasText(originalIdp.getAliasZid())) { - // no mirroring is necessary - return originalIdp; - } - - final IdentityProvider mirroredIdp = new IdentityProvider<>() - .setActive(originalIdp.isActive()) - .setConfig(originalIdp.getConfig()) - .setName(originalIdp.getName()) - .setOriginKey(originalIdp.getOriginKey()) - .setType(originalIdp.getType()) - // reference the ID and zone ID of the initial IdP entry - .setAliasZid(originalIdp.getIdentityZoneId()) - .setAliasId(originalIdp.getId()) - .setIdentityZoneId(originalIdp.getAliasZid()); - mirroredIdp.setSerializeConfigRaw(originalIdp.isSerializeConfigRaw()); - - // get the referenced, mirrored IdP - final IdentityProvider existingMirroredIdp; - if (hasText(originalIdp.getAliasId())) { - // if the referenced IdP does not exist, we create a new one - existingMirroredIdp = retrieveMirroredIdp(originalIdp); - } else { - existingMirroredIdp = null; - } - - // update the existing mirrored IdP - if (existingMirroredIdp != null) { - mirroredIdp.setId(existingMirroredIdp.getId()); - identityProviderProvisioning.update(mirroredIdp, originalIdp.getAliasZid()); - return originalIdp; - } - - // check if IdZ referenced in 'aliasZid' exists - try { - identityZoneProvisioning.retrieve(originalIdp.getAliasZid()); - } catch (final ZoneDoesNotExistsException e) { - throw new IdpMirroringFailedException(String.format( - "Could not mirror IdP '%s' to zone '%s', as zone does not exist.", - originalIdp.getId(), - originalIdp.getAliasZid() - ), e); - } - - // create new mirrored IdP in alias zid - final IdentityProvider persistedMirroredIdp = identityProviderProvisioning.create( - mirroredIdp, - originalIdp.getAliasZid() - ); - - // update alias ID in original IdP - originalIdp.setAliasId(persistedMirroredIdp.getId()); - return identityProviderProvisioning.update(originalIdp, originalIdp.getIdentityZoneId()); - } - - private IdentityProvider retrieveMirroredIdp(final IdentityProvider originalIdp) { - try { - return identityProviderProvisioning.retrieve( - originalIdp.getAliasId(), - originalIdp.getAliasZid() - ); - } catch (final EmptyResultDataAccessException e) { - logger.warn( - "The IdP referenced in the 'aliasId' ('{}') and 'aliasZid' ('{}') of the IdP '{}' does not exist.", - originalIdp.getAliasId(), - originalIdp.getAliasZid(), - originalIdp.getId() - ); - return null; - } - } - @RequestMapping(value = "{id}", method = DELETE) @Transactional public ResponseEntity deleteIdentityProvider(@PathVariable String id, @RequestParam(required = false, defaultValue = "false") boolean rawConfig) { @@ -308,7 +200,7 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } - if (!isValidAliasPropertyUpdate(body, existing)) { + if (!aliasPropertiesAreValid(body, existing)) { logger.error("IdentityProvider[origin="+body.getOriginKey()+"; zone="+body.getIdentityZoneId()+"] - Alias ID and/or ZID changed during update of already mirrored IdP."); return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } @@ -332,46 +224,6 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str return new ResponseEntity<>(updatedIdp, OK); } - /** - * Checks whether an update operation is valid in regard to the alias properties. - * - * @param updatePayload the updated version of the IdP to be persisted - * @param existingIdp the existing version of the IdP - * @return whether the update of the alias properties is valid - */ - private static boolean isValidAliasPropertyUpdate( - final IdentityProvider updatePayload, - final IdentityProvider existingIdp - ) { - if (!hasText(existingIdp.getAliasId()) && !hasText(existingIdp.getAliasZid())) { - // no alias properties set previously - - if (hasText(updatePayload.getAliasId())) { - return false; // 'aliasId' must be empty - } - - if (!hasText(updatePayload.getAliasZid())) { - return true; // no mirroring necessary - } - - // one of the zones must be "uaa" - return updatePayload.getAliasZid().equals(UAA) || updatePayload.getIdentityZoneId().equals(UAA); - } - - if (!hasText(existingIdp.getAliasId()) || !hasText(existingIdp.getAliasZid())) { - // at this point, we expect both properties to be set -> if not, the IdP is in an inconsistent state - throw new IllegalStateException(String.format( - "Both alias ID and alias ZID expected to be set for IdP '%s' in zone '%s'.", - existingIdp.getId(), - existingIdp.getIdentityZoneId() - )); - } - - // both properties must be equal in the update payload - return existingIdp.getAliasId().equals(updatePayload.getAliasId()) - && existingIdp.getAliasZid().equals(updatePayload.getAliasZid()); - } - @RequestMapping (value = "{id}/status", method = PATCH) public ResponseEntity updateIdentityProviderStatus(@PathVariable String id, @RequestBody IdentityProviderStatus body) { String zoneId = identityZoneManager.getCurrentIdentityZoneId(); @@ -472,6 +324,143 @@ public ResponseEntity testIdentityProvider(@RequestBody IdentityProvider return new ResponseEntity<>(JsonUtils.writeValueAsString(exception), status); } + private boolean aliasPropertiesAreValid( + @NonNull final IdentityProvider requestBody, + @Nullable final IdentityProvider existingIdp + ) { + // if the IdP was already mirrored, the alias properties must not be changed + final boolean idpWasAlreadyMirrored = existingIdp != null && hasText(existingIdp.getAliasZid()); + if (idpWasAlreadyMirrored) { + if (!hasText(existingIdp.getAliasId())) { + // at this point, we expect both properties to be set -> if not, the IdP is in an inconsistent state + throw new IllegalStateException(String.format( + "Both alias ID and alias ZID expected to be set for IdP '%s' in zone '%s'.", + existingIdp.getId(), + existingIdp.getIdentityZoneId() + )); + } + + // both alias properties must be equal in the update payload + return existingIdp.getAliasId().equals(requestBody.getAliasId()) + && existingIdp.getAliasZid().equals(requestBody.getAliasZid()); + } + + // if the IdP was not mirrored already, the aliasId must be empty + if (hasText(requestBody.getAliasId())) { + return false; + } + + // check if mirroring is necessary + if (!hasText(requestBody.getAliasZid())) { + return true; + } + + // the referenced zone must exist + try { + identityZoneProvisioning.retrieve(requestBody.getAliasZid()); + } catch (final ZoneDoesNotExistsException e) { + logger.debug( + "IdentityProvider[origin={}; zone={}] - Zone referenced in alias zone ID does not exist.", + requestBody.getOriginKey(), + requestBody.getIdentityZoneId() + ); + return false; + } + + // 'identityZoneId' and 'aliasZid' must not be equal + if (requestBody.getIdentityZoneId().equals(requestBody.getAliasZid())) { + return false; + } + + // one of the zones must be 'uaa' + return requestBody.getIdentityZoneId().equals(UAA) || requestBody.getAliasZid().equals(UAA); + } + + /** + * Ensure consistency during create or update operations with a mirrored IdP referenced in the original IdPs alias + * properties. If the IdP has both its alias ID and alias ZID set, the existing mirrored IdP is updated. If only + * the alias ZID is set, a new mirrored IdP is created. + * This method should be executed in a transaction together with the original create or update operation. + * + * @param originalIdp the original IdP; must be persisted, i.e., have an ID, already + * @return the original IdP after the operation, with a potentially updated "aliasId" field + * @throws IdpMirroringFailedException if a new mirrored IdP needs to be created, but the zone referenced in + * 'aliasZid' does not exist + * @throws IdpMirroringFailedException if 'aliasId' and 'aliasZid' are set in the original IdP, but the referenced + * mirrored IdP could not be found + */ + private IdentityProvider ensureConsistencyOfMirroredIdp(final IdentityProvider originalIdp) throws IdpMirroringFailedException { + if (!hasText(originalIdp.getAliasZid())) { + // no mirroring is necessary + return originalIdp; + } + + final IdentityProvider mirroredIdp = new IdentityProvider<>() + .setActive(originalIdp.isActive()) + .setConfig(originalIdp.getConfig()) + .setName(originalIdp.getName()) + .setOriginKey(originalIdp.getOriginKey()) + .setType(originalIdp.getType()) + // reference the ID and zone ID of the initial IdP entry + .setAliasZid(originalIdp.getIdentityZoneId()) + .setAliasId(originalIdp.getId()) + .setIdentityZoneId(originalIdp.getAliasZid()); + mirroredIdp.setSerializeConfigRaw(originalIdp.isSerializeConfigRaw()); + + // get the referenced, mirrored IdP + final IdentityProvider existingMirroredIdp; + if (hasText(originalIdp.getAliasId())) { + // if the referenced IdP does not exist, we create a new one + existingMirroredIdp = retrieveMirroredIdp(originalIdp); + } else { + existingMirroredIdp = null; + } + + // update the existing mirrored IdP + if (existingMirroredIdp != null) { + mirroredIdp.setId(existingMirroredIdp.getId()); + identityProviderProvisioning.update(mirroredIdp, originalIdp.getAliasZid()); + return originalIdp; + } + + // check if IdZ referenced in 'aliasZid' exists + try { + identityZoneProvisioning.retrieve(originalIdp.getAliasZid()); + } catch (final ZoneDoesNotExistsException e) { + throw new IdpMirroringFailedException(String.format( + "Could not mirror IdP '%s' to zone '%s', as zone does not exist.", + originalIdp.getId(), + originalIdp.getAliasZid() + ), e); + } + + // create new mirrored IdP in alias zid + final IdentityProvider persistedMirroredIdp = identityProviderProvisioning.create( + mirroredIdp, + originalIdp.getAliasZid() + ); + + // update alias ID in original IdP + originalIdp.setAliasId(persistedMirroredIdp.getId()); + return identityProviderProvisioning.update(originalIdp, originalIdp.getIdentityZoneId()); + } + + private IdentityProvider retrieveMirroredIdp(final IdentityProvider originalIdp) { + try { + return identityProviderProvisioning.retrieve( + originalIdp.getAliasId(), + originalIdp.getAliasZid() + ); + } catch (final EmptyResultDataAccessException e) { + logger.warn( + "The IdP referenced in the 'aliasId' ('{}') and 'aliasZid' ('{}') of the IdP '{}' does not exist.", + originalIdp.getAliasId(), + originalIdp.getAliasZid(), + originalIdp.getId() + ); + return null; + } + } @ExceptionHandler(MetadataProviderException.class) public ResponseEntity handleMetadataProviderException(MetadataProviderException e) { diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 7df7d12c7ac..6112eef088a 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -351,6 +351,26 @@ void shouldReject_IdpInCustomZoneMirroredToOtherCustomZone() throws Exception { shouldReject(customZone, idpInCustomZone, HttpStatus.UNPROCESSABLE_ENTITY); } + @Test + void shouldReject_AliasZidSetToSameZone_UaaZone() throws Exception { + shouldReject_AliasZidSetToSameZone(IdentityZone.getUaa()); + } + + @Test + void shouldReject_AliasZidSetToSameZone_CustomZone() throws Exception { + shouldReject_AliasZidSetToSameZone(customZone); + } + + private void shouldReject_AliasZidSetToSameZone(final IdentityZone zone) throws Exception { + final IdentityProvider idp = createIdp( + zone, + buildIdpWithAliasProperties(zone.getId(), null, null), + getAccessTokenForZone(zone) + ); + idp.setAliasZid(zone.getId()); + shouldReject(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); + } + private IdentityProvider updateIdp( final IdentityZone zone, final IdentityProvider updatePayload, From 1d2e9aec6b74b4f96721176e925538802280bb95 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 20 Dec 2023 16:50:18 +0100 Subject: [PATCH 23/91] Add tests for update status calls involving mirrored IdPs --- ...ityProviderEndpointsAliasMockMvcTests.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 6112eef088a..0a7d94f39a2 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -11,10 +11,13 @@ 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.patch; 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; import java.util.Arrays; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.Optional; @@ -27,7 +30,10 @@ import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; import org.cloudfoundry.identity.uaa.oauth.token.TokenConstants; import org.cloudfoundry.identity.uaa.provider.IdentityProvider; +import org.cloudfoundry.identity.uaa.provider.IdentityProviderStatus; +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.BootstrapSamlIdentityProviderDataTests; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.test.TestClient; @@ -459,6 +465,69 @@ private void shouldDeleteMirroredIdp(final IdentityZone zone1, final IdentityZon } } + @Nested + class UpdateStatus { + @Test + void shouldAccept_MirroredIdpShouldAlsoBeUpdated_UaaToCustomZone() throws Exception { + shouldAccept_MirroredIdpShouldAlsoBeUpdated(IdentityZone.getUaa(), customZone); + } + + @Test + void shouldAccept_MirroredIdpShouldAlsoBeUpdated_CustomToUaaZone() throws Exception { + shouldAccept_MirroredIdpShouldAlsoBeUpdated(customZone, IdentityZone.getUaa()); + } + + private void shouldAccept_MirroredIdpShouldAlsoBeUpdated(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + // create an IdP of type UAA + final IdentityProvider idp = new IdentityProvider<>(); + idp.setType(OriginKeys.UAA); + idp.setName("some-name"); + idp.setOriginKey(RandomStringUtils.randomAlphabetic(8)); + final PasswordPolicy passwordPolicy = new PasswordPolicy(); + passwordPolicy.setExpirePasswordInMonths(1); + passwordPolicy.setMaxLength(100); + passwordPolicy.setMinLength(10); + passwordPolicy.setRequireDigit(1); + passwordPolicy.setRequireUpperCaseCharacter(1); + passwordPolicy.setRequireLowerCaseCharacter(1); + passwordPolicy.setRequireSpecialCharacter(1); + passwordPolicy.setPasswordNewerThan(new Date(System.currentTimeMillis())); + idp.setConfig(new UaaIdentityProviderDefinition(passwordPolicy, null)); + idp.setAliasZid(zone2.getId()); + final String accessTokenForZone1 = getAccessTokenForZone(zone1); + final IdentityProvider createdIdp = createIdp(zone1, idp, accessTokenForZone1); + + final Date timestampBeforeUpdate = getPasswordNewerThanTimestamp(createdIdp); + assertThat(timestampBeforeUpdate).isNotNull(); + + final IdentityProviderStatus identityProviderStatus = new IdentityProviderStatus(); + identityProviderStatus.setRequirePasswordChange(true); + final MockHttpServletRequestBuilder updateRequestBuilder = patch("/identity-providers/" + createdIdp.getId() + "/status") + .header("Authorization", "Bearer " + accessTokenForZone1) + .header(IdentityZoneSwitchingFilter.HEADER, zone1.getId()) + .contentType(APPLICATION_JSON) + .content(JsonUtils.writeValueAsString(identityProviderStatus)); + mockMvc.perform(updateRequestBuilder).andExpect(status().isOk()).andReturn(); + + // check if timestamp is updated in zone 1 + final Optional idpInZone1 = readIdpFromZoneIfExists(zone1, createdIdp.getId(), accessTokenForZone1); + assertThat(idpInZone1).isPresent(); + final Date timestampAfterUpdate = getPasswordNewerThanTimestamp(idpInZone1.get()); + assertThat(timestampAfterUpdate).isAfter(timestampBeforeUpdate); + + // check if timestamp is updated in zone 2 + final String accessTokenForZone2 = getAccessTokenForZone(zone2); + final Optional idpInZone2 = readIdpFromZoneIfExists(zone2, createdIdp.getAliasId(), accessTokenForZone2); + assertThat(idpInZone2).isPresent(); + final Date timestampAfterUpdateMirroredIdp = getPasswordNewerThanTimestamp(idpInZone2.get()); + assertThat(timestampAfterUpdateMirroredIdp).isEqualTo(timestampAfterUpdate); + } + + private Date getPasswordNewerThanTimestamp(final IdentityProvider idp) { + return ((UaaIdentityProviderDefinition) idp.getConfig()).getPasswordPolicy().getPasswordNewerThan(); + } + } + private void assertIdpReferencesOtherIdp(final IdentityProvider idp, final IdentityProvider referencedIdp) { assertThat(idp).isNotNull(); assertThat(referencedIdp).isNotNull(); From 63472c437f4591a95eb93fb8847ae5b9291d15bc Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 20 Dec 2023 17:31:37 +0100 Subject: [PATCH 24/91] Add tests for mirrored IdP being created if referenced, but not present --- ...ityProviderEndpointsAliasMockMvcTests.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 0a7d94f39a2..03492869d19 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -31,6 +31,7 @@ import org.cloudfoundry.identity.uaa.oauth.token.TokenConstants; import org.cloudfoundry.identity.uaa.provider.IdentityProvider; import org.cloudfoundry.identity.uaa.provider.IdentityProviderStatus; +import org.cloudfoundry.identity.uaa.provider.JdbcIdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.provider.PasswordPolicy; import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.UaaIdentityProviderDefinition; @@ -52,6 +53,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.web.context.WebApplicationContext; @DefaultTestContext class IdentityProviderEndpointsAliasMockMvcTests { @@ -61,6 +63,9 @@ class IdentityProviderEndpointsAliasMockMvcTests { @Autowired private TestClient testClient; + @Autowired + private WebApplicationContext webApplicationContext; + private IdentityZone customZone; private String adminToken; private String identityToken; @@ -259,6 +264,37 @@ private void shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged(final Id assertThat(mirroredIdp.get().getName()).isNotBlank().isEqualTo(newName); } + @Test + void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp_UaaToCustomZone() throws Exception { + shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp(IdentityZone.getUaa(), customZone); + } + + @Test + void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp_CustomToUaaZone() throws Exception { + shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp(customZone, IdentityZone.getUaa()); + } + + private void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + final IdentityProvider idp = createMirroredIdp(zone1, zone2); + + // delete the mirrored IdP directly in the DB -> after that, there is a dangling reference + final JdbcIdentityProviderProvisioning identityProviderProvisioning = webApplicationContext.getBean(JdbcIdentityProviderProvisioning.class); + final int rowsDeleted = identityProviderProvisioning.deleteByOrigin(idp.getOriginKey(), zone2.getId()); + assertThat(rowsDeleted).isEqualTo(1); + + // update some other property on the original IdP + idp.setName("some-new-name"); + final IdentityProvider updatedIdp = updateIdp(zone1, idp, getAccessTokenForZone(zone1)); + assertThat(updatedIdp.getAliasId()).isNotBlank().isNotEqualTo(idp.getAliasId()); + assertThat(updatedIdp.getAliasZid()).isNotBlank().isEqualTo(idp.getAliasZid()); + + // check if the new mirrored IdP is present and has the correct properties + final Optional mirroredIdp = readIdpFromZoneIfExists(zone2, updatedIdp.getAliasId(), getAccessTokenForZone(zone2)); + assertThat(mirroredIdp).isPresent(); + assertIdpReferencesOtherIdp(updatedIdp, mirroredIdp.get()); + assertOtherPropertiesAreEqual(updatedIdp, mirroredIdp.get()); + } + @ParameterizedTest @MethodSource("shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp") void shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp_UaaToCustomZone(final String newAliasId, final String newAliasZid) throws Exception { From bf5c42c1abb30a3e27f9d55538a4d0901f8c3779 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 20 Dec 2023 17:41:05 +0100 Subject: [PATCH 25/91] Add deletion of mirrored IdPs when Identity Zone is deleted --- .../uaa/provider/JdbcIdentityProviderProvisioning.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 1fe515e9875..2f8bd80c58a 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 @@ -40,7 +40,7 @@ public class JdbcIdentityProviderProvisioning implements IdentityProviderProvisi 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 DELETE_IDENTITY_PROVIDER_BY_ZONE_SQL = "delete from identity_provider where identity_zone_id=? or alias_zid=?"; public static final String IDENTITY_PROVIDER_BY_ID_QUERY = "select " + ID_PROVIDER_FIELDS + " from identity_provider " + "where id=? and identity_zone_id=?"; @@ -152,7 +152,7 @@ protected void validate(IdentityProvider provider) { @Override public int deleteByIdentityZone(String zoneId) { - return jdbcTemplate.update(DELETE_IDENTITY_PROVIDER_BY_ZONE_SQL, zoneId); + return jdbcTemplate.update(DELETE_IDENTITY_PROVIDER_BY_ZONE_SQL, zoneId, zoneId); } @Override From d985b8fe19ec6911a65ab8a185f9d31b397d0c06 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 21 Dec 2023 10:13:51 +0100 Subject: [PATCH 26/91] Add test for mirrored IdPs being removed when zones are deleted --- ...JdbcIdentityProviderProvisioningTests.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java index 9886d9d928f..c942f37ee75 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java @@ -1,5 +1,7 @@ package org.cloudfoundry.identity.uaa.provider; +import org.apache.commons.lang.RandomStringUtils; +import org.assertj.core.api.Assertions; import org.cloudfoundry.identity.uaa.annotations.WithDatabaseContext; import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent; import org.cloudfoundry.identity.uaa.constants.OriginKeys; @@ -16,6 +18,7 @@ import java.sql.Timestamp; import java.util.List; import java.util.Map; +import java.util.UUID; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; import static org.cloudfoundry.identity.uaa.zone.IdentityZone.getUaaZoneId; @@ -63,6 +66,52 @@ void deleteProvidersInZone() { assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=?", new Object[]{otherZoneId1}, Integer.class), is(0)); } + @Test + void deleteProvidersInUaaZone_Mirrored() { + final IdentityZone mockIdentityZone = mock(IdentityZone.class); + when(mockIdentityZone.getId()).thenReturn(otherZoneId1); + + final String originSuffix = RandomStringUtils.randomAlphabetic(5); + + // IdP 1: not mirrored + final IdentityProvider idp1 = MultitenancyFixture.identityProvider("origin1-" + originSuffix, otherZoneId1); + final IdentityProvider createdIdp1 = jdbcIdentityProviderProvisioning.create(idp1, otherZoneId1); + Assertions.assertThat(createdIdp1).isNotNull(); + Assertions.assertThat(createdIdp1.getId()).isNotBlank(); + + // IdP 2: mirrored to UAA zone + final String idp2Id = UUID.randomUUID().toString(); + final String idp2MirroredId = UUID.randomUUID().toString(); + final String origin2 = "origin2-" + originSuffix; + final IdentityProvider idp2 = MultitenancyFixture.identityProvider(origin2, otherZoneId1); + idp2.setId(idp2Id); + idp2.setAliasZid(uaaZoneId); + idp2.setAliasId(idp2MirroredId); + final IdentityProvider createdIdp2 = jdbcIdentityProviderProvisioning.create(idp2, otherZoneId1); + Assertions.assertThat(createdIdp2).isNotNull(); + Assertions.assertThat(createdIdp2.getId()).isNotBlank(); + final IdentityProvider idp2Mirrored = MultitenancyFixture.identityProvider(origin2, uaaZoneId); + idp2Mirrored.setId(idp2MirroredId); + idp2Mirrored.setAliasZid(otherZoneId1); + idp2Mirrored.setAliasId(idp2Id); + final IdentityProvider createdIdp2Mirrored = jdbcIdentityProviderProvisioning.create(idp2Mirrored, uaaZoneId); + Assertions.assertThat(createdIdp2Mirrored).isNotNull(); + Assertions.assertThat(createdIdp2Mirrored.getId()).isNotBlank(); + + // check if all three entries are present in the DB + Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{otherZoneId1, createdIdp1.getId()}, Integer.class)).isEqualTo(1); + Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{otherZoneId1, createdIdp2.getId()}, Integer.class)).isEqualTo(1); + Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{uaaZoneId, createdIdp2Mirrored.getId()}, Integer.class)).isEqualTo(1); + + // emit custom zone deleted event + jdbcIdentityProviderProvisioning.onApplicationEvent(new EntityDeletedEvent<>(mockIdentityZone, null, otherZoneId1)); + + // check if all three entries are gone + Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{otherZoneId1, createdIdp1.getId()}, Integer.class)).isEqualTo(0); + Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{otherZoneId1, createdIdp2.getId()}, Integer.class)).isEqualTo(0); + Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{uaaZoneId, createdIdp2Mirrored.getId()}, Integer.class)).isEqualTo(0); + } + @Test void deleteProvidersInUaaZone() { IdentityProvider idp = MultitenancyFixture.identityProvider(origin, uaaZoneId); From 05acd82e654720398f4bdc44c97599e7684577e1 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 21 Dec 2023 11:53:16 +0100 Subject: [PATCH 27/91] Add tests for transaction handling --- ...ityProviderEndpointsAliasMockMvcTests.java | 214 ++++++++++++------ 1 file changed, 141 insertions(+), 73 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 03492869d19..4f01590c5f6 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -3,11 +3,8 @@ import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; 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; @@ -15,6 +12,7 @@ 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; +import static org.springframework.util.StringUtils.hasText; import java.util.Arrays; import java.util.Date; @@ -55,6 +53,11 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.web.context.WebApplicationContext; +import com.fasterxml.jackson.core.type.TypeReference; + +/** + * Tests regarding the handling of "aliasId" and "aliasZid" properties of identity providers. + */ @DefaultTestContext class IdentityProviderEndpointsAliasMockMvcTests { @Autowired @@ -100,14 +103,15 @@ private void shouldAccept_MirrorIdp(final IdentityZone zone1, final IdentityZone final IdentityProvider provider = buildIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); // create IdP in zone1 - final IdentityProvider originalIdp = createIdp(zone1, provider, getAccessTokenForZone(zone1)); + final IdentityProvider originalIdp = createIdp(zone1, provider, getAccessTokenForZone(zone1.getId())); assertThat(originalIdp).isNotNull(); assertThat(originalIdp.getAliasId()).isNotBlank(); assertThat(originalIdp.getAliasZid()).isNotBlank().isEqualTo(zone2.getId()); // read mirrored IdP from zone2 - final String accessTokenZone2 = getAccessTokenForZone(zone2); - final Optional mirroredIdp = readIdpFromZoneIfExists(zone2, originalIdp.getAliasId(), accessTokenZone2); + final String accessTokenZone2 = getAccessTokenForZone(zone2.getId()); + final String id = originalIdp.getAliasId(); + final Optional mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id, accessTokenZone2); assertThat(mirroredIdp).isPresent(); assertIdpReferencesOtherIdp(mirroredIdp.get(), originalIdp); assertOtherPropertiesAreEqual(originalIdp, mirroredIdp.get()); @@ -128,21 +132,21 @@ void shouldReject_IdzAndAliasZidAreEqual_CustomZone() throws Exception { private void shouldReject_IdzAndAliasZidAreEqual(final IdentityZone zone) throws Exception { final IdentityProvider idp = buildIdpWithAliasProperties(zone.getId(), null, zone.getId()); - shouldReject(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); + shouldRejectCreation(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } @Test void shouldReject_NeitherIdzNorAliasZidIsUaa() throws Exception { final IdentityZone otherCustomZone = MockMvcUtils.createZoneUsingWebRequest(mockMvc, identityToken); final IdentityProvider idp = buildIdpWithAliasProperties(customZone.getId(), null, otherCustomZone.getId()); - shouldReject(customZone, idp, HttpStatus.UNPROCESSABLE_ENTITY); + shouldRejectCreation(customZone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } @Test void shouldReject_AliasIdIsSet() throws Exception { final String aliasId = UUID.randomUUID().toString(); final IdentityProvider idp = buildIdpWithAliasProperties(customZone.getId(), aliasId, IdentityZone.getUaaZoneId()); - shouldReject(customZone, idp, HttpStatus.UNPROCESSABLE_ENTITY); + shouldRejectCreation(customZone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } @Test @@ -152,7 +156,7 @@ void shouldReject_IdzReferencedInAliasZidDoesNotExist() throws Exception { null, UUID.randomUUID().toString() // does not exist ); - shouldReject(IdentityZone.getUaa(), provider, HttpStatus.UNPROCESSABLE_ENTITY); + shouldRejectCreation(IdentityZone.getUaa(), provider, HttpStatus.UNPROCESSABLE_ENTITY); } @Test @@ -172,21 +176,29 @@ private void shouldReject_IdpWithOriginAlreadyExistsInAliasZone(final IdentityZo final IdentityProvider createdIdp1 = createIdp( zone1, buildIdpWithAliasProperties(zone1.getId(), null, null, originKey), - getAccessTokenForZone(zone1) + getAccessTokenForZone(zone1.getId()) ); - assertNotNull(createdIdp1); + assertThat(createdIdp1).isNotNull(); // then, create an IdP in the "uaa" zone with the same origin key that should be mirrored to the custom zone - shouldReject( + shouldRejectCreation( zone2, buildIdpWithAliasProperties(zone2.getId(), null, zone1.getId(), originKey), HttpStatus.CONFLICT ); } - private void shouldReject(final IdentityZone zone, final IdentityProvider idp, final HttpStatus expectedStatus) throws Exception { - final MvcResult result = createIdpAndReturnResult(zone, idp, getAccessTokenForZone(zone)); + private void shouldRejectCreation(final IdentityZone zone, final IdentityProvider idp, final HttpStatus expectedStatus) throws Exception { + assertThat(expectedStatus.isError()).isTrue(); + + final String accessTokenForZone = getAccessTokenForZone(zone.getId()); + final MvcResult result = createIdpAndReturnResult(zone, idp, accessTokenForZone); assertThat(result.getResponse().getStatus()).isEqualTo(expectedStatus.value()); + + // after the failed creation, the IdP must not exist + final List idpsInZoneAfterFailedCreation = readAllIdpsInZone(zone, accessTokenForZone); + assertThat(idpsInZoneAfterFailedCreation.stream().map(IdentityProvider::getOriginKey).collect(toSet())) + .doesNotContain(idp.getOriginKey()); } } @@ -203,7 +215,7 @@ void shouldAccept_ShouldCreateMirroredIdp_CustomToUaaZone() throws Exception { } private void shouldAccept_ShouldCreateMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final String accessTokenForZone1 = getAccessTokenForZone(zone1); + final String accessTokenForZone1 = getAccessTokenForZone(zone1.getId()); // create regular idp without alias properties in UAA zone final IdentityProvider existingIdpWithoutAlias = createIdp( @@ -211,20 +223,20 @@ private void shouldAccept_ShouldCreateMirroredIdp(final IdentityZone zone1, fina buildIdpWithAliasProperties(zone1.getId(), null, null), accessTokenForZone1 ); - assertNotNull(existingIdpWithoutAlias); - assertNotNull(existingIdpWithoutAlias.getId()); + assertThat(existingIdpWithoutAlias).isNotNull(); + assertThat(existingIdpWithoutAlias.getId()).isNotBlank(); // perform update: set Alias ZID existingIdpWithoutAlias.setAliasZid(zone2.getId()); final IdentityProvider idpAfterUpdate = updateIdp(zone1, existingIdpWithoutAlias, accessTokenForZone1); - assertNotNull(idpAfterUpdate.getAliasId()); - assertNotNull(idpAfterUpdate.getAliasZid()); - assertEquals(zone2.getId(), idpAfterUpdate.getAliasZid()); + assertThat(idpAfterUpdate.getAliasId()).isNotBlank(); + assertThat(idpAfterUpdate.getAliasZid()).isNotBlank(); + assertThat(zone2.getId()).isEqualTo(idpAfterUpdate.getAliasZid()); // read mirrored IdP through alias id in original IdP - final String accessTokenForZone2 = getAccessTokenForZone(zone2); + final String accessTokenForZone2 = getAccessTokenForZone(zone2.getId()); final String id = idpAfterUpdate.getAliasId(); - final Optional idp = readIdpFromZoneIfExists(zone2, id, accessTokenForZone2); + final Optional idp = readIdpFromZoneIfExists(zone2.getId(), id, accessTokenForZone2); assertThat(idp).isPresent(); final IdentityProvider mirroredIdp = idp.get(); assertIdpReferencesOtherIdp(mirroredIdp, idpAfterUpdate); @@ -248,7 +260,7 @@ private void shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged(final Id // update other property final String newName = "new name"; originalIdp.setName(newName); - final IdentityProvider updatedOriginalIdp = updateIdp(zone1, originalIdp, getAccessTokenForZone(zone1)); + final IdentityProvider updatedOriginalIdp = updateIdp(zone1, originalIdp, getAccessTokenForZone(zone1.getId())); assertThat(updatedOriginalIdp).isNotNull(); assertThat(updatedOriginalIdp.getAliasId()).isNotBlank(); assertThat(updatedOriginalIdp.getAliasZid()).isNotBlank(); @@ -256,9 +268,9 @@ private void shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged(final Id assertThat(updatedOriginalIdp.getName()).isNotBlank().isEqualTo(newName); // check if the change is propagated to the mirrored IdP - final String accessTokenZone2 = getAccessTokenForZone(zone2); + final String accessTokenZone2 = getAccessTokenForZone(zone2.getId()); final String id = updatedOriginalIdp.getAliasId(); - final Optional mirroredIdp = readIdpFromZoneIfExists(zone2, id, accessTokenZone2); + final Optional mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id, accessTokenZone2); assertThat(mirroredIdp).isPresent(); assertIdpReferencesOtherIdp(mirroredIdp.get(), updatedOriginalIdp); assertThat(mirroredIdp.get().getName()).isNotBlank().isEqualTo(newName); @@ -284,12 +296,14 @@ private void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp(fi // update some other property on the original IdP idp.setName("some-new-name"); - final IdentityProvider updatedIdp = updateIdp(zone1, idp, getAccessTokenForZone(zone1)); + final IdentityProvider updatedIdp = updateIdp(zone1, idp, getAccessTokenForZone(zone1.getId())); assertThat(updatedIdp.getAliasId()).isNotBlank().isNotEqualTo(idp.getAliasId()); assertThat(updatedIdp.getAliasZid()).isNotBlank().isEqualTo(idp.getAliasZid()); // check if the new mirrored IdP is present and has the correct properties - final Optional mirroredIdp = readIdpFromZoneIfExists(zone2, updatedIdp.getAliasId(), getAccessTokenForZone(zone2)); + final String id = updatedIdp.getAliasId(); + final String accessTokenForZone = getAccessTokenForZone(zone2.getId()); + final Optional mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id, accessTokenForZone); assertThat(mirroredIdp).isPresent(); assertIdpReferencesOtherIdp(updatedIdp, mirroredIdp.get()); assertOtherPropertiesAreEqual(updatedIdp, mirroredIdp.get()); @@ -316,7 +330,7 @@ private void shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp( final IdentityProvider originalIdp = createMirroredIdp(zone1, zone2); originalIdp.setAliasId(newAliasId); originalIdp.setAliasZid(newAliasZid); - shouldReject(zone1, originalIdp, HttpStatus.UNPROCESSABLE_ENTITY); + shouldRejectUpdate(zone1, originalIdp, HttpStatus.UNPROCESSABLE_ENTITY); } private static Stream shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp() { @@ -338,10 +352,10 @@ void shouldReject_OnlyAliasIdSet_CustomZone() throws Exception { private void shouldReject_OnlyAliasIdSet(final IdentityZone zone) throws Exception { final IdentityProvider idp = buildIdpWithAliasProperties(zone.getId(), null, null); - final IdentityProvider createdProvider = createIdp(zone, idp, getAccessTokenForZone(zone)); - assertNull(createdProvider.getAliasZid()); + final IdentityProvider createdProvider = createIdp(zone, idp, getAccessTokenForZone(zone.getId())); + assertThat(createdProvider.getAliasZid()).isBlank(); createdProvider.setAliasId(UUID.randomUUID().toString()); - shouldReject(zone, createdProvider, HttpStatus.UNPROCESSABLE_ENTITY); + shouldRejectUpdate(zone, createdProvider, HttpStatus.UNPROCESSABLE_ENTITY); } @Test @@ -364,7 +378,7 @@ private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final Identi null, originKey ); - createIdp(zone2, existingIdpInZone2, getAccessTokenForZone(zone2)); + createIdp(zone2, existingIdpInZone2, getAccessTokenForZone(zone2.getId())); // create IdP with same origin key in zone 1 final IdentityProvider idp = buildIdpWithAliasProperties( @@ -373,11 +387,11 @@ private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final Identi null, originKey // same origin key ); - final IdentityProvider providerInZone1 = createIdp(zone1, idp, getAccessTokenForZone(zone1)); + final IdentityProvider providerInZone1 = createIdp(zone1, idp, getAccessTokenForZone(zone1.getId())); // update the alias ZID to zone 2, where an IdP with this origin already exists -> should fail providerInZone1.setAliasZid(zone2.getId()); - shouldReject(zone1, providerInZone1, HttpStatus.INTERNAL_SERVER_ERROR); + shouldRejectUpdate(zone1, providerInZone1, HttpStatus.INTERNAL_SERVER_ERROR); } @Test @@ -385,12 +399,12 @@ void shouldReject_IdpInCustomZoneMirroredToOtherCustomZone() throws Exception { final IdentityProvider idpInCustomZone = createIdp( customZone, buildIdpWithAliasProperties(customZone.getId(), null, null), - getAccessTokenForZone(customZone) + getAccessTokenForZone(customZone.getId()) ); // try to mirror it to another custom zone idpInCustomZone.setAliasZid("not-uaa"); - shouldReject(customZone, idpInCustomZone, HttpStatus.UNPROCESSABLE_ENTITY); + shouldRejectUpdate(customZone, idpInCustomZone, HttpStatus.UNPROCESSABLE_ENTITY); } @Test @@ -407,10 +421,10 @@ private void shouldReject_AliasZidSetToSameZone(final IdentityZone zone) throws final IdentityProvider idp = createIdp( zone, buildIdpWithAliasProperties(zone.getId(), null, null), - getAccessTokenForZone(zone) + getAccessTokenForZone(zone.getId()) ); idp.setAliasZid(zone.getId()); - shouldReject(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); + shouldRejectUpdate(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } private IdentityProvider updateIdp( @@ -420,15 +434,15 @@ private IdentityProvider updateIdp( ) throws Exception { updatePayload.setIdentityZoneId(zone.getId()); final MvcResult result = updateIdpAndReturnResult(zone, updatePayload, accessTokenForZone); - assertEquals(HttpStatus.OK.value(), result.getResponse().getStatus()); + assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); final IdentityProvider originalIdpAfterUpdate = JsonUtils.readValue( result.getResponse().getContentAsString(), IdentityProvider.class ); - assertNotNull(originalIdpAfterUpdate); - assertNotNull(originalIdpAfterUpdate.getIdentityZoneId()); - assertEquals(zone.getId(), originalIdpAfterUpdate.getIdentityZoneId()); + assertThat(originalIdpAfterUpdate).isNotNull(); + assertThat(originalIdpAfterUpdate.getIdentityZoneId()).isNotBlank(); + assertThat(originalIdpAfterUpdate.getIdentityZoneId()).isEqualTo(zone.getId()); return originalIdpAfterUpdate; } @@ -448,9 +462,55 @@ private MvcResult updateIdpAndReturnResult( return mockMvc.perform(updateRequestBuilder).andReturn(); } - private void shouldReject(final IdentityZone zone, final IdentityProvider idp, final HttpStatus expectedStatus) throws Exception { - final MvcResult result = updateIdpAndReturnResult(zone, idp, getAccessTokenForZone(zone)); - assertThat(result.getResponse().getStatus()).isEqualTo(expectedStatus.value()); + private void shouldRejectUpdate(final IdentityZone zone, final IdentityProvider idp, final HttpStatus expectedErrorStatus) throws Exception { + assertThat(idp.getId()).isNotBlank(); + assertThat(expectedErrorStatus.isError()).isTrue(); + + // read existing IdP before update + final String accessTokenForZone = getAccessTokenForZone(zone.getId()); + final String id = idp.getId(); + final Optional idpBeforeUpdateOpt = readIdpFromZoneIfExists(zone.getId(), id, accessTokenForZone); + assertThat(idpBeforeUpdateOpt).isPresent(); + final IdentityProvider idpBeforeUpdate = idpBeforeUpdateOpt.get(); + + // if alias properties set: read mirrored IdP before update + final String accessTokenForAliasZone; + final IdentityProvider mirroredIdpBeforeUpdate; + if (hasText(idpBeforeUpdate.getAliasId()) && hasText(idpBeforeUpdate.getAliasZid())) { + accessTokenForAliasZone = getAccessTokenForZone(idpBeforeUpdate.getAliasZid()); + final Optional mirroredIdpBeforeUpdateOpt = readIdpFromZoneIfExists( + idpBeforeUpdate.getAliasZid(), + idpBeforeUpdate.getAliasId(), + accessTokenForAliasZone + ); + assertThat(mirroredIdpBeforeUpdateOpt).isPresent(); + mirroredIdpBeforeUpdate = mirroredIdpBeforeUpdateOpt.get(); + } else { + accessTokenForAliasZone = null; + mirroredIdpBeforeUpdate = null; + } + + // perform the update -> should fail + final MvcResult result = updateIdpAndReturnResult(zone, idp, accessTokenForZone); + assertThat(result.getResponse().getStatus()).isEqualTo(expectedErrorStatus.value()); + + // read again: original IdP should remain unchanged + final Optional idpAfterFailedUpdateOpt = readIdpFromZoneIfExists( + zone.getId(), + idp.getId(), + accessTokenForZone + ); + assertThat(idpAfterFailedUpdateOpt).isPresent().contains(idpBeforeUpdate); + + // if a mirrored IdP was present before update, check if it also remains unchanged + if (mirroredIdpBeforeUpdate != null) { + final Optional mirroredIdpAfterFailedUpdateOpt = readIdpFromZoneIfExists( + idpBeforeUpdate.getAliasZid(), + idpBeforeUpdate.getAliasId(), + accessTokenForAliasZone + ); + assertThat(mirroredIdpAfterFailedUpdateOpt).isPresent().contains(mirroredIdpBeforeUpdate); + } } } @@ -476,14 +536,14 @@ private void shouldDeleteMirroredIdp(final IdentityZone zone1, final IdentityZon assertThat(aliasZid).isNotBlank().isEqualTo(zone2.getId()); // check if mirrored IdP is available in zone 2 - final String accessTokenForZone2 = getAccessTokenForZone(zone2); - final Optional mirroredIdp = readIdpFromZoneIfExists(zone2, aliasId, accessTokenForZone2); + final String accessTokenForZone2 = getAccessTokenForZone(zone2.getId()); + final Optional mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), aliasId, accessTokenForZone2); assertThat(mirroredIdp).isPresent(); assertThat(mirroredIdp.get().getAliasId()).isNotBlank().isEqualTo(id); assertThat(mirroredIdp.get().getAliasZid()).isNotBlank().isEqualTo(idpInZone1.getIdentityZoneId()); // delete IdP in zone 1 - final String accessTokenForZone1 = getAccessTokenForZone(zone1); + final String accessTokenForZone1 = getAccessTokenForZone(zone1.getId()); final MockHttpServletRequestBuilder deleteRequestBuilder = delete("/identity-providers/" + id) .header("Authorization", "Bearer " + accessTokenForZone1) .header(IdentityZoneSwitchingFilter.HEADER, zone1.getId()); @@ -492,11 +552,7 @@ private void shouldDeleteMirroredIdp(final IdentityZone zone1, final IdentityZon assertThat(response.getResponse().getStatus()).isEqualTo(200); // check if IdP is no longer available in zone 2 - final Optional mirroredIdpAfterDeletionOfOriginalIdp = readIdpFromZoneIfExists( - zone2, - aliasId, - accessTokenForZone2 - ); + final Optional mirroredIdpAfterDeletionOfOriginalIdp = readIdpFromZoneIfExists(zone2.getId(), aliasId, accessTokenForZone2); assertThat(mirroredIdpAfterDeletionOfOriginalIdp).isNotPresent(); } } @@ -530,7 +586,7 @@ private void shouldAccept_MirroredIdpShouldAlsoBeUpdated(final IdentityZone zone passwordPolicy.setPasswordNewerThan(new Date(System.currentTimeMillis())); idp.setConfig(new UaaIdentityProviderDefinition(passwordPolicy, null)); idp.setAliasZid(zone2.getId()); - final String accessTokenForZone1 = getAccessTokenForZone(zone1); + final String accessTokenForZone1 = getAccessTokenForZone(zone1.getId()); final IdentityProvider createdIdp = createIdp(zone1, idp, accessTokenForZone1); final Date timestampBeforeUpdate = getPasswordNewerThanTimestamp(createdIdp); @@ -546,14 +602,16 @@ private void shouldAccept_MirroredIdpShouldAlsoBeUpdated(final IdentityZone zone mockMvc.perform(updateRequestBuilder).andExpect(status().isOk()).andReturn(); // check if timestamp is updated in zone 1 - final Optional idpInZone1 = readIdpFromZoneIfExists(zone1, createdIdp.getId(), accessTokenForZone1); + final String id1 = createdIdp.getId(); + final Optional idpInZone1 = readIdpFromZoneIfExists(zone1.getId(), id1, accessTokenForZone1); assertThat(idpInZone1).isPresent(); final Date timestampAfterUpdate = getPasswordNewerThanTimestamp(idpInZone1.get()); assertThat(timestampAfterUpdate).isAfter(timestampBeforeUpdate); // check if timestamp is updated in zone 2 - final String accessTokenForZone2 = getAccessTokenForZone(zone2); - final Optional idpInZone2 = readIdpFromZoneIfExists(zone2, createdIdp.getAliasId(), accessTokenForZone2); + final String accessTokenForZone2 = getAccessTokenForZone(zone2.getId()); + final String id = createdIdp.getAliasId(); + final Optional idpInZone2 = readIdpFromZoneIfExists(zone2.getId(), id, accessTokenForZone2); assertThat(idpInZone2).isPresent(); final Date timestampAfterUpdateMirroredIdp = getPasswordNewerThanTimestamp(idpInZone2.get()); assertThat(timestampAfterUpdateMirroredIdp).isEqualTo(timestampAfterUpdate); @@ -577,17 +635,17 @@ private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final Ide originalIdpConfig.setZoneId(null); final SamlIdentityProviderDefinition mirroredIdpConfig = (SamlIdentityProviderDefinition) mirroredIdp.getConfig(); mirroredIdpConfig.setZoneId(null); - assertEquals(originalIdpConfig, mirroredIdpConfig); + assertThat(mirroredIdpConfig).isEqualTo(originalIdpConfig); // check if remaining properties are equal - assertEquals(idp.getOriginKey(), mirroredIdp.getOriginKey()); - assertEquals(idp.getName(), mirroredIdp.getName()); - assertEquals(idp.getType(), mirroredIdp.getType()); + assertThat(mirroredIdp.getOriginKey()).isEqualTo(idp.getOriginKey()); + assertThat(mirroredIdp.getName()).isEqualTo(idp.getName()); + assertThat(mirroredIdp.getType()).isEqualTo(idp.getType()); } private IdentityProvider createMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { final IdentityProvider provider = buildIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); - return createIdp(zone1, provider, getAccessTokenForZone(zone1)); + return createIdp(zone1, provider, getAccessTokenForZone(zone1.getId())); } private IdentityProvider createIdp(final IdentityZone zone, final IdentityProvider idp, final String accessTokenForZone) throws Exception { @@ -606,8 +664,8 @@ private MvcResult createIdpAndReturnResult(final IdentityZone zone, final Identi return mockMvc.perform(createRequestBuilder).andReturn(); } - private String getAccessTokenForZone(final IdentityZone zone) throws Exception { - final List scopesForZone = getScopesForZone(zone, "admin"); + private String getAccessTokenForZone(final String zoneId) throws Exception { + final List scopesForZone = getScopesForZone(zoneId, "admin"); final ScimUser adminUser = MockMvcUtils.createAdminForZone( mockMvc, @@ -629,8 +687,8 @@ private String getAccessTokenForZone(final IdentityZone zone) throws Exception { // check if the token contains the expected scopes final Map claims = UaaTokenUtils.getClaims(accessToken); - assertTrue(claims.containsKey("scope")); - assertTrue(claims.get("scope") instanceof List); + assertThat(claims).containsKey("scope"); + assertThat(claims.get("scope")).isInstanceOf(List.class); final List resultingScopes = (List) claims.get("scope"); assertThat(resultingScopes).hasSameElementsAs(scopesForZone); @@ -638,13 +696,13 @@ private String getAccessTokenForZone(final IdentityZone zone) throws Exception { } private Optional readIdpFromZoneIfExists( - final IdentityZone zone, + final String zoneId, final String id, final String accessTokenForZone ) throws Exception { final MockHttpServletRequestBuilder getRequestBuilder = get("/identity-providers/" + id) .param("rawConfig", "true") - .header(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER, zone.getSubdomain()) + .header(IdentityZoneSwitchingFilter.HEADER, zoneId) .header("Authorization", "Bearer " + accessTokenForZone); final MvcResult getResult = mockMvc.perform(getRequestBuilder).andReturn(); final int responseStatus = getResult.getResponse().getStatus(); @@ -658,15 +716,25 @@ private Optional readIdpFromZoneIfExists( getResult.getResponse().getContentAsString(), IdentityProvider.class ); - return Optional.of(responseBody); + return Optional.ofNullable(responseBody); default: // should not happen return Optional.empty(); } } - private static List getScopesForZone(final IdentityZone zone, final String... scopes) { - return Stream.of(scopes).map(scope -> String.format("zones.%s.%s", zone.getId(), scope)).collect(toList()); + private List readAllIdpsInZone(final IdentityZone zone, final String accessTokenForZone) throws Exception { + final MockHttpServletRequestBuilder getRequestBuilder = get("/identity-providers") + .param("rawConfig", "true") + .header(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER, zone.getSubdomain()) + .header("Authorization", "Bearer " + accessTokenForZone); + final MvcResult getResult = mockMvc.perform(getRequestBuilder).andExpect(status().isOk()).andReturn(); + return JsonUtils.readValue(getResult.getResponse().getContentAsString(), new TypeReference<>() { + }); + } + + private static List getScopesForZone(final String zoneId, final String... scopes) { + return Stream.of(scopes).map(scope -> String.format("zones.%s.%s", zoneId, scope)).collect(toList()); } private static IdentityProvider buildIdpWithAliasProperties(final String idzId, final String aliasId, final String aliasZid) { From 7cde84ccdc494f29c2f6187c60039efe99264481 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 21 Dec 2023 12:06:41 +0100 Subject: [PATCH 28/91] Add caching of access tokens in IdentityProviderEndpointsAliasMockMvcTests --- ...ityProviderEndpointsAliasMockMvcTests.java | 127 ++++++++---------- 1 file changed, 53 insertions(+), 74 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 4f01590c5f6..6575105d1d5 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -16,6 +16,7 @@ import java.util.Arrays; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -60,6 +61,8 @@ */ @DefaultTestContext class IdentityProviderEndpointsAliasMockMvcTests { + private static final Map ACCESS_TOKEN_CACHE = new HashMap<>(); + @Autowired private MockMvc mockMvc; @@ -103,21 +106,20 @@ private void shouldAccept_MirrorIdp(final IdentityZone zone1, final IdentityZone final IdentityProvider provider = buildIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); // create IdP in zone1 - final IdentityProvider originalIdp = createIdp(zone1, provider, getAccessTokenForZone(zone1.getId())); + final IdentityProvider originalIdp = createIdp(zone1, provider); assertThat(originalIdp).isNotNull(); assertThat(originalIdp.getAliasId()).isNotBlank(); assertThat(originalIdp.getAliasZid()).isNotBlank().isEqualTo(zone2.getId()); // read mirrored IdP from zone2 - final String accessTokenZone2 = getAccessTokenForZone(zone2.getId()); final String id = originalIdp.getAliasId(); - final Optional mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id, accessTokenZone2); + final Optional mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id); assertThat(mirroredIdp).isPresent(); assertIdpReferencesOtherIdp(mirroredIdp.get(), originalIdp); assertOtherPropertiesAreEqual(originalIdp, mirroredIdp.get()); // check if aliasId in first IdP is equal to the ID of the mirrored one - assertThat(originalIdp.getAliasId()).isEqualTo(mirroredIdp.get().getId()); + assertThat(mirroredIdp.get().getId()).isEqualTo(originalIdp.getAliasId()); } @Test @@ -175,8 +177,7 @@ private void shouldReject_IdpWithOriginAlreadyExistsInAliasZone(final IdentityZo // create IdP with origin key in custom zone final IdentityProvider createdIdp1 = createIdp( zone1, - buildIdpWithAliasProperties(zone1.getId(), null, null, originKey), - getAccessTokenForZone(zone1.getId()) + buildIdpWithAliasProperties(zone1.getId(), null, null, originKey) ); assertThat(createdIdp1).isNotNull(); @@ -191,12 +192,11 @@ private void shouldReject_IdpWithOriginAlreadyExistsInAliasZone(final IdentityZo private void shouldRejectCreation(final IdentityZone zone, final IdentityProvider idp, final HttpStatus expectedStatus) throws Exception { assertThat(expectedStatus.isError()).isTrue(); - final String accessTokenForZone = getAccessTokenForZone(zone.getId()); - final MvcResult result = createIdpAndReturnResult(zone, idp, accessTokenForZone); + final MvcResult result = createIdpAndReturnResult(zone, idp); assertThat(result.getResponse().getStatus()).isEqualTo(expectedStatus.value()); // after the failed creation, the IdP must not exist - final List idpsInZoneAfterFailedCreation = readAllIdpsInZone(zone, accessTokenForZone); + final List idpsInZoneAfterFailedCreation = readAllIdpsInZone(zone); assertThat(idpsInZoneAfterFailedCreation.stream().map(IdentityProvider::getOriginKey).collect(toSet())) .doesNotContain(idp.getOriginKey()); } @@ -215,28 +215,24 @@ void shouldAccept_ShouldCreateMirroredIdp_CustomToUaaZone() throws Exception { } private void shouldAccept_ShouldCreateMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final String accessTokenForZone1 = getAccessTokenForZone(zone1.getId()); - // create regular idp without alias properties in UAA zone final IdentityProvider existingIdpWithoutAlias = createIdp( zone1, - buildIdpWithAliasProperties(zone1.getId(), null, null), - accessTokenForZone1 + buildIdpWithAliasProperties(zone1.getId(), null, null) ); assertThat(existingIdpWithoutAlias).isNotNull(); assertThat(existingIdpWithoutAlias.getId()).isNotBlank(); // perform update: set Alias ZID existingIdpWithoutAlias.setAliasZid(zone2.getId()); - final IdentityProvider idpAfterUpdate = updateIdp(zone1, existingIdpWithoutAlias, accessTokenForZone1); + final IdentityProvider idpAfterUpdate = updateIdp(zone1, existingIdpWithoutAlias); assertThat(idpAfterUpdate.getAliasId()).isNotBlank(); assertThat(idpAfterUpdate.getAliasZid()).isNotBlank(); assertThat(zone2.getId()).isEqualTo(idpAfterUpdate.getAliasZid()); // read mirrored IdP through alias id in original IdP - final String accessTokenForZone2 = getAccessTokenForZone(zone2.getId()); final String id = idpAfterUpdate.getAliasId(); - final Optional idp = readIdpFromZoneIfExists(zone2.getId(), id, accessTokenForZone2); + final Optional idp = readIdpFromZoneIfExists(zone2.getId(), id); assertThat(idp).isPresent(); final IdentityProvider mirroredIdp = idp.get(); assertIdpReferencesOtherIdp(mirroredIdp, idpAfterUpdate); @@ -260,7 +256,7 @@ private void shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged(final Id // update other property final String newName = "new name"; originalIdp.setName(newName); - final IdentityProvider updatedOriginalIdp = updateIdp(zone1, originalIdp, getAccessTokenForZone(zone1.getId())); + final IdentityProvider updatedOriginalIdp = updateIdp(zone1, originalIdp); assertThat(updatedOriginalIdp).isNotNull(); assertThat(updatedOriginalIdp.getAliasId()).isNotBlank(); assertThat(updatedOriginalIdp.getAliasZid()).isNotBlank(); @@ -268,9 +264,8 @@ private void shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged(final Id assertThat(updatedOriginalIdp.getName()).isNotBlank().isEqualTo(newName); // check if the change is propagated to the mirrored IdP - final String accessTokenZone2 = getAccessTokenForZone(zone2.getId()); final String id = updatedOriginalIdp.getAliasId(); - final Optional mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id, accessTokenZone2); + final Optional mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id); assertThat(mirroredIdp).isPresent(); assertIdpReferencesOtherIdp(mirroredIdp.get(), updatedOriginalIdp); assertThat(mirroredIdp.get().getName()).isNotBlank().isEqualTo(newName); @@ -296,14 +291,13 @@ private void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp(fi // update some other property on the original IdP idp.setName("some-new-name"); - final IdentityProvider updatedIdp = updateIdp(zone1, idp, getAccessTokenForZone(zone1.getId())); + final IdentityProvider updatedIdp = updateIdp(zone1, idp); assertThat(updatedIdp.getAliasId()).isNotBlank().isNotEqualTo(idp.getAliasId()); assertThat(updatedIdp.getAliasZid()).isNotBlank().isEqualTo(idp.getAliasZid()); // check if the new mirrored IdP is present and has the correct properties final String id = updatedIdp.getAliasId(); - final String accessTokenForZone = getAccessTokenForZone(zone2.getId()); - final Optional mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id, accessTokenForZone); + final Optional mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id); assertThat(mirroredIdp).isPresent(); assertIdpReferencesOtherIdp(updatedIdp, mirroredIdp.get()); assertOtherPropertiesAreEqual(updatedIdp, mirroredIdp.get()); @@ -352,7 +346,7 @@ void shouldReject_OnlyAliasIdSet_CustomZone() throws Exception { private void shouldReject_OnlyAliasIdSet(final IdentityZone zone) throws Exception { final IdentityProvider idp = buildIdpWithAliasProperties(zone.getId(), null, null); - final IdentityProvider createdProvider = createIdp(zone, idp, getAccessTokenForZone(zone.getId())); + final IdentityProvider createdProvider = createIdp(zone, idp); assertThat(createdProvider.getAliasZid()).isBlank(); createdProvider.setAliasId(UUID.randomUUID().toString()); shouldRejectUpdate(zone, createdProvider, HttpStatus.UNPROCESSABLE_ENTITY); @@ -378,7 +372,7 @@ private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final Identi null, originKey ); - createIdp(zone2, existingIdpInZone2, getAccessTokenForZone(zone2.getId())); + createIdp(zone2, existingIdpInZone2); // create IdP with same origin key in zone 1 final IdentityProvider idp = buildIdpWithAliasProperties( @@ -387,7 +381,7 @@ private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final Identi null, originKey // same origin key ); - final IdentityProvider providerInZone1 = createIdp(zone1, idp, getAccessTokenForZone(zone1.getId())); + final IdentityProvider providerInZone1 = createIdp(zone1, idp); // update the alias ZID to zone 2, where an IdP with this origin already exists -> should fail providerInZone1.setAliasZid(zone2.getId()); @@ -398,8 +392,7 @@ private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final Identi void shouldReject_IdpInCustomZoneMirroredToOtherCustomZone() throws Exception { final IdentityProvider idpInCustomZone = createIdp( customZone, - buildIdpWithAliasProperties(customZone.getId(), null, null), - getAccessTokenForZone(customZone.getId()) + buildIdpWithAliasProperties(customZone.getId(), null, null) ); // try to mirror it to another custom zone @@ -420,20 +413,15 @@ void shouldReject_AliasZidSetToSameZone_CustomZone() throws Exception { private void shouldReject_AliasZidSetToSameZone(final IdentityZone zone) throws Exception { final IdentityProvider idp = createIdp( zone, - buildIdpWithAliasProperties(zone.getId(), null, null), - getAccessTokenForZone(zone.getId()) + buildIdpWithAliasProperties(zone.getId(), null, null) ); idp.setAliasZid(zone.getId()); shouldRejectUpdate(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } - private IdentityProvider updateIdp( - final IdentityZone zone, - final IdentityProvider updatePayload, - final String accessTokenForZone - ) throws Exception { + private IdentityProvider updateIdp(final IdentityZone zone, final IdentityProvider updatePayload) throws Exception { updatePayload.setIdentityZoneId(zone.getId()); - final MvcResult result = updateIdpAndReturnResult(zone, updatePayload, accessTokenForZone); + final MvcResult result = updateIdpAndReturnResult(zone, updatePayload); assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); final IdentityProvider originalIdpAfterUpdate = JsonUtils.readValue( @@ -446,16 +434,12 @@ private IdentityProvider updateIdp( return originalIdpAfterUpdate; } - private MvcResult updateIdpAndReturnResult( - final IdentityZone zone, - final IdentityProvider updatePayload, - final String accessTokenForZone - ) throws Exception { + private MvcResult updateIdpAndReturnResult(final IdentityZone zone, final IdentityProvider updatePayload) throws Exception { final String id = updatePayload.getId(); assertThat(id).isNotNull().isNotBlank(); final MockHttpServletRequestBuilder updateRequestBuilder = put("/identity-providers/" + id) - .header("Authorization", "Bearer " + accessTokenForZone) + .header("Authorization", "Bearer " + getAccessTokenForZone(zone.getId())) .header(IdentityZoneSwitchingFilter.HEADER, zone.getId()) .contentType(APPLICATION_JSON) .content(JsonUtils.writeValueAsString(updatePayload)); @@ -467,38 +451,32 @@ private void shouldRejectUpdate(final IdentityZone zone, final IdentityProvider assertThat(expectedErrorStatus.isError()).isTrue(); // read existing IdP before update - final String accessTokenForZone = getAccessTokenForZone(zone.getId()); final String id = idp.getId(); - final Optional idpBeforeUpdateOpt = readIdpFromZoneIfExists(zone.getId(), id, accessTokenForZone); + final Optional idpBeforeUpdateOpt = readIdpFromZoneIfExists(zone.getId(), id); assertThat(idpBeforeUpdateOpt).isPresent(); final IdentityProvider idpBeforeUpdate = idpBeforeUpdateOpt.get(); // if alias properties set: read mirrored IdP before update - final String accessTokenForAliasZone; final IdentityProvider mirroredIdpBeforeUpdate; if (hasText(idpBeforeUpdate.getAliasId()) && hasText(idpBeforeUpdate.getAliasZid())) { - accessTokenForAliasZone = getAccessTokenForZone(idpBeforeUpdate.getAliasZid()); final Optional mirroredIdpBeforeUpdateOpt = readIdpFromZoneIfExists( idpBeforeUpdate.getAliasZid(), - idpBeforeUpdate.getAliasId(), - accessTokenForAliasZone + idpBeforeUpdate.getAliasId() ); assertThat(mirroredIdpBeforeUpdateOpt).isPresent(); mirroredIdpBeforeUpdate = mirroredIdpBeforeUpdateOpt.get(); } else { - accessTokenForAliasZone = null; mirroredIdpBeforeUpdate = null; } // perform the update -> should fail - final MvcResult result = updateIdpAndReturnResult(zone, idp, accessTokenForZone); + final MvcResult result = updateIdpAndReturnResult(zone, idp); assertThat(result.getResponse().getStatus()).isEqualTo(expectedErrorStatus.value()); // read again: original IdP should remain unchanged final Optional idpAfterFailedUpdateOpt = readIdpFromZoneIfExists( zone.getId(), - idp.getId(), - accessTokenForZone + idp.getId() ); assertThat(idpAfterFailedUpdateOpt).isPresent().contains(idpBeforeUpdate); @@ -506,8 +484,7 @@ private void shouldRejectUpdate(final IdentityZone zone, final IdentityProvider if (mirroredIdpBeforeUpdate != null) { final Optional mirroredIdpAfterFailedUpdateOpt = readIdpFromZoneIfExists( idpBeforeUpdate.getAliasZid(), - idpBeforeUpdate.getAliasId(), - accessTokenForAliasZone + idpBeforeUpdate.getAliasId() ); assertThat(mirroredIdpAfterFailedUpdateOpt).isPresent().contains(mirroredIdpBeforeUpdate); } @@ -536,8 +513,7 @@ private void shouldDeleteMirroredIdp(final IdentityZone zone1, final IdentityZon assertThat(aliasZid).isNotBlank().isEqualTo(zone2.getId()); // check if mirrored IdP is available in zone 2 - final String accessTokenForZone2 = getAccessTokenForZone(zone2.getId()); - final Optional mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), aliasId, accessTokenForZone2); + final Optional mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), aliasId); assertThat(mirroredIdp).isPresent(); assertThat(mirroredIdp.get().getAliasId()).isNotBlank().isEqualTo(id); assertThat(mirroredIdp.get().getAliasZid()).isNotBlank().isEqualTo(idpInZone1.getIdentityZoneId()); @@ -547,12 +523,12 @@ private void shouldDeleteMirroredIdp(final IdentityZone zone1, final IdentityZon final MockHttpServletRequestBuilder deleteRequestBuilder = delete("/identity-providers/" + id) .header("Authorization", "Bearer " + accessTokenForZone1) .header(IdentityZoneSwitchingFilter.HEADER, zone1.getId()); - final var response = mockMvc.perform(deleteRequestBuilder).andReturn(); + final MvcResult response = mockMvc.perform(deleteRequestBuilder).andReturn(); assertThat(response.getResponse().getStatus()).isEqualTo(200); // check if IdP is no longer available in zone 2 - final Optional mirroredIdpAfterDeletionOfOriginalIdp = readIdpFromZoneIfExists(zone2.getId(), aliasId, accessTokenForZone2); + final Optional mirroredIdpAfterDeletionOfOriginalIdp = readIdpFromZoneIfExists(zone2.getId(), aliasId); assertThat(mirroredIdpAfterDeletionOfOriginalIdp).isNotPresent(); } } @@ -587,7 +563,7 @@ private void shouldAccept_MirroredIdpShouldAlsoBeUpdated(final IdentityZone zone idp.setConfig(new UaaIdentityProviderDefinition(passwordPolicy, null)); idp.setAliasZid(zone2.getId()); final String accessTokenForZone1 = getAccessTokenForZone(zone1.getId()); - final IdentityProvider createdIdp = createIdp(zone1, idp, accessTokenForZone1); + final IdentityProvider createdIdp = createIdp(zone1, idp); final Date timestampBeforeUpdate = getPasswordNewerThanTimestamp(createdIdp); assertThat(timestampBeforeUpdate).isNotNull(); @@ -603,15 +579,14 @@ private void shouldAccept_MirroredIdpShouldAlsoBeUpdated(final IdentityZone zone // check if timestamp is updated in zone 1 final String id1 = createdIdp.getId(); - final Optional idpInZone1 = readIdpFromZoneIfExists(zone1.getId(), id1, accessTokenForZone1); + final Optional idpInZone1 = readIdpFromZoneIfExists(zone1.getId(), id1); assertThat(idpInZone1).isPresent(); final Date timestampAfterUpdate = getPasswordNewerThanTimestamp(idpInZone1.get()); assertThat(timestampAfterUpdate).isAfter(timestampBeforeUpdate); // check if timestamp is updated in zone 2 - final String accessTokenForZone2 = getAccessTokenForZone(zone2.getId()); final String id = createdIdp.getAliasId(); - final Optional idpInZone2 = readIdpFromZoneIfExists(zone2.getId(), id, accessTokenForZone2); + final Optional idpInZone2 = readIdpFromZoneIfExists(zone2.getId(), id); assertThat(idpInZone2).isPresent(); final Date timestampAfterUpdateMirroredIdp = getPasswordNewerThanTimestamp(idpInZone2.get()); assertThat(timestampAfterUpdateMirroredIdp).isEqualTo(timestampAfterUpdate); @@ -645,19 +620,19 @@ private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final Ide private IdentityProvider createMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { final IdentityProvider provider = buildIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); - return createIdp(zone1, provider, getAccessTokenForZone(zone1.getId())); + return createIdp(zone1, provider); } - private IdentityProvider createIdp(final IdentityZone zone, final IdentityProvider idp, final String accessTokenForZone) throws Exception { - final MvcResult createResult = createIdpAndReturnResult(zone, idp, accessTokenForZone); + private IdentityProvider createIdp(final IdentityZone zone, final IdentityProvider idp) throws Exception { + final MvcResult createResult = createIdpAndReturnResult(zone, idp); assertThat(createResult.getResponse().getStatus()).isEqualTo(HttpStatus.CREATED.value()); return JsonUtils.readValue(createResult.getResponse().getContentAsString(), IdentityProvider.class); } - private MvcResult createIdpAndReturnResult(final IdentityZone zone, final IdentityProvider idp, final String accessTokenForZone) throws Exception { + private MvcResult createIdpAndReturnResult(final IdentityZone zone, final IdentityProvider idp) throws Exception { final MockHttpServletRequestBuilder createRequestBuilder = post("/identity-providers") .param("rawConfig", "true") - .header("Authorization", "Bearer " + accessTokenForZone) + .header("Authorization", "Bearer " + getAccessTokenForZone(zone.getId())) .header(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER, zone.getSubdomain()) .contentType(APPLICATION_JSON) .content(JsonUtils.writeValueAsString(idp)); @@ -665,6 +640,11 @@ private MvcResult createIdpAndReturnResult(final IdentityZone zone, final Identi } private String getAccessTokenForZone(final String zoneId) throws Exception { + final String cacheLookupResult = ACCESS_TOKEN_CACHE.get(zoneId); + if (cacheLookupResult != null) { + return cacheLookupResult; + } + final List scopesForZone = getScopesForZone(zoneId, "admin"); final ScimUser adminUser = MockMvcUtils.createAdminForZone( @@ -692,18 +672,17 @@ private String getAccessTokenForZone(final String zoneId) throws Exception { final List resultingScopes = (List) claims.get("scope"); assertThat(resultingScopes).hasSameElementsAs(scopesForZone); + // cache the access token + ACCESS_TOKEN_CACHE.put(zoneId, accessToken); + return accessToken; } - private Optional readIdpFromZoneIfExists( - final String zoneId, - final String id, - final String accessTokenForZone - ) throws Exception { + private Optional readIdpFromZoneIfExists(final String zoneId, final String id) throws Exception { final MockHttpServletRequestBuilder getRequestBuilder = get("/identity-providers/" + id) .param("rawConfig", "true") .header(IdentityZoneSwitchingFilter.HEADER, zoneId) - .header("Authorization", "Bearer " + accessTokenForZone); + .header("Authorization", "Bearer " + getAccessTokenForZone(zoneId)); final MvcResult getResult = mockMvc.perform(getRequestBuilder).andReturn(); final int responseStatus = getResult.getResponse().getStatus(); assertThat(responseStatus).isIn(404, 200); @@ -723,11 +702,11 @@ private Optional readIdpFromZoneIfExists( } } - private List readAllIdpsInZone(final IdentityZone zone, final String accessTokenForZone) throws Exception { + private List readAllIdpsInZone(final IdentityZone zone) throws Exception { final MockHttpServletRequestBuilder getRequestBuilder = get("/identity-providers") .param("rawConfig", "true") .header(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER, zone.getSubdomain()) - .header("Authorization", "Bearer " + accessTokenForZone); + .header("Authorization", "Bearer " + getAccessTokenForZone(zone.getId())); final MvcResult getResult = mockMvc.perform(getRequestBuilder).andExpect(status().isOk()).andReturn(); return JsonUtils.readValue(getResult.getResponse().getContentAsString(), new TypeReference<>() { }); From 9106d3215cd837edddf94499779f0ea75311cbda Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 21 Dec 2023 13:10:59 +0100 Subject: [PATCH 29/91] Fix Sonar finding --- .../identity/uaa/provider/IdentityProviderEndpoints.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 274af91ec0e..8da91bea1ba 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 @@ -218,9 +218,13 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str final IdentityProvider updatedOriginalIdp = identityProviderProvisioning.update(body, zoneId); return ensureConsistencyOfMirroredIdp(updatedOriginalIdp); }); - + if (updatedIdp == null) { + logger.error("IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "] - Transaction updating IdP and mirrored IdP was not successful, but no exception was thrown."); + return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); + } updatedIdp.setSerializeConfigRaw(rawConfig); redactSensitiveData(updatedIdp); + return new ResponseEntity<>(updatedIdp, OK); } From d10465f4ae787dbc30be9caf73673c9faa941c6a Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 21 Dec 2023 13:25:35 +0100 Subject: [PATCH 30/91] Fix Sonar code smells --- .../provider/IdentityProviderEndpoints.java | 38 +++--- ...JdbcIdentityProviderProvisioningTests.java | 6 +- ...ityProviderEndpointsAliasMockMvcTests.java | 112 +++++++++--------- 3 files changed, 79 insertions(+), 77 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 8da91bea1ba..fd4e34b8725 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 @@ -214,8 +214,8 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str body.setConfig(definition); } - final IdentityProvider updatedIdp = transactionTemplate.execute(txStatus -> { - final IdentityProvider updatedOriginalIdp = identityProviderProvisioning.update(body, zoneId); + final IdentityProvider updatedIdp = transactionTemplate.execute(txStatus -> { + final IdentityProvider updatedOriginalIdp = identityProviderProvisioning.update(body, zoneId); return ensureConsistencyOfMirroredIdp(updatedOriginalIdp); }); if (updatedIdp == null) { @@ -329,8 +329,8 @@ public ResponseEntity testIdentityProvider(@RequestBody IdentityProvider } private boolean aliasPropertiesAreValid( - @NonNull final IdentityProvider requestBody, - @Nullable final IdentityProvider existingIdp + @NonNull final IdentityProvider requestBody, + @Nullable final IdentityProvider existingIdp ) { // if the IdP was already mirrored, the alias properties must not be changed final boolean idpWasAlreadyMirrored = existingIdp != null && hasText(existingIdp.getAliasZid()); @@ -393,26 +393,28 @@ private boolean aliasPropertiesAreValid( * @throws IdpMirroringFailedException if 'aliasId' and 'aliasZid' are set in the original IdP, but the referenced * mirrored IdP could not be found */ - private IdentityProvider ensureConsistencyOfMirroredIdp(final IdentityProvider originalIdp) throws IdpMirroringFailedException { + private IdentityProvider ensureConsistencyOfMirroredIdp( + final IdentityProvider originalIdp + ) throws IdpMirroringFailedException { if (!hasText(originalIdp.getAliasZid())) { // no mirroring is necessary return originalIdp; } - final IdentityProvider mirroredIdp = new IdentityProvider<>() - .setActive(originalIdp.isActive()) - .setConfig(originalIdp.getConfig()) - .setName(originalIdp.getName()) - .setOriginKey(originalIdp.getOriginKey()) - .setType(originalIdp.getType()) - // reference the ID and zone ID of the initial IdP entry - .setAliasZid(originalIdp.getIdentityZoneId()) - .setAliasId(originalIdp.getId()) - .setIdentityZoneId(originalIdp.getAliasZid()); + final IdentityProvider mirroredIdp = new IdentityProvider<>(); + mirroredIdp.setActive(originalIdp.isActive()); + mirroredIdp.setName(originalIdp.getName()); + mirroredIdp.setOriginKey(originalIdp.getOriginKey()); + mirroredIdp.setType(originalIdp.getType()); + mirroredIdp.setConfig(originalIdp.getConfig()); mirroredIdp.setSerializeConfigRaw(originalIdp.isSerializeConfigRaw()); + // reference the ID and zone ID of the initial IdP entry + mirroredIdp.setAliasZid(originalIdp.getIdentityZoneId()); + mirroredIdp.setAliasId(originalIdp.getId()); + mirroredIdp.setIdentityZoneId(originalIdp.getAliasZid()); // get the referenced, mirrored IdP - final IdentityProvider existingMirroredIdp; + final IdentityProvider existingMirroredIdp; if (hasText(originalIdp.getAliasId())) { // if the referenced IdP does not exist, we create a new one existingMirroredIdp = retrieveMirroredIdp(originalIdp); @@ -439,7 +441,7 @@ private IdentityProvider ensureConsistencyOfMirroredIdp(final IdentityProvider o } // create new mirrored IdP in alias zid - final IdentityProvider persistedMirroredIdp = identityProviderProvisioning.create( + final IdentityProvider persistedMirroredIdp = identityProviderProvisioning.create( mirroredIdp, originalIdp.getAliasZid() ); @@ -449,7 +451,7 @@ private IdentityProvider ensureConsistencyOfMirroredIdp(final IdentityProvider o return identityProviderProvisioning.update(originalIdp, originalIdp.getIdentityZoneId()); } - private IdentityProvider retrieveMirroredIdp(final IdentityProvider originalIdp) { + private IdentityProvider retrieveMirroredIdp(final IdentityProvider originalIdp) { try { return identityProviderProvisioning.retrieve( originalIdp.getAliasId(), diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java index c942f37ee75..f1c463bd379 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java @@ -107,9 +107,9 @@ void deleteProvidersInUaaZone_Mirrored() { jdbcIdentityProviderProvisioning.onApplicationEvent(new EntityDeletedEvent<>(mockIdentityZone, null, otherZoneId1)); // check if all three entries are gone - Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{otherZoneId1, createdIdp1.getId()}, Integer.class)).isEqualTo(0); - Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{otherZoneId1, createdIdp2.getId()}, Integer.class)).isEqualTo(0); - Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{uaaZoneId, createdIdp2Mirrored.getId()}, Integer.class)).isEqualTo(0); + Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{otherZoneId1, createdIdp1.getId()}, Integer.class)).isZero(); + Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{otherZoneId1, createdIdp2.getId()}, Integer.class)).isZero(); + Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{uaaZoneId, createdIdp2Mirrored.getId()}, Integer.class)).isZero(); } @Test diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 6575105d1d5..6abbac25565 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -103,17 +103,17 @@ void shouldAccept_MirrorIdp_CustomToUaaZone() throws Exception { private void shouldAccept_MirrorIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { // build IdP in zone1 with aliasZid set to zone2 - final IdentityProvider provider = buildIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); + final IdentityProvider provider = buildIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); // create IdP in zone1 - final IdentityProvider originalIdp = createIdp(zone1, provider); + final IdentityProvider originalIdp = createIdp(zone1, provider); assertThat(originalIdp).isNotNull(); assertThat(originalIdp.getAliasId()).isNotBlank(); assertThat(originalIdp.getAliasZid()).isNotBlank().isEqualTo(zone2.getId()); // read mirrored IdP from zone2 final String id = originalIdp.getAliasId(); - final Optional mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id); + final Optional> mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id); assertThat(mirroredIdp).isPresent(); assertIdpReferencesOtherIdp(mirroredIdp.get(), originalIdp); assertOtherPropertiesAreEqual(originalIdp, mirroredIdp.get()); @@ -133,27 +133,27 @@ void shouldReject_IdzAndAliasZidAreEqual_CustomZone() throws Exception { } private void shouldReject_IdzAndAliasZidAreEqual(final IdentityZone zone) throws Exception { - final IdentityProvider idp = buildIdpWithAliasProperties(zone.getId(), null, zone.getId()); + final IdentityProvider idp = buildIdpWithAliasProperties(zone.getId(), null, zone.getId()); shouldRejectCreation(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } @Test void shouldReject_NeitherIdzNorAliasZidIsUaa() throws Exception { final IdentityZone otherCustomZone = MockMvcUtils.createZoneUsingWebRequest(mockMvc, identityToken); - final IdentityProvider idp = buildIdpWithAliasProperties(customZone.getId(), null, otherCustomZone.getId()); + final IdentityProvider idp = buildIdpWithAliasProperties(customZone.getId(), null, otherCustomZone.getId()); shouldRejectCreation(customZone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } @Test void shouldReject_AliasIdIsSet() throws Exception { final String aliasId = UUID.randomUUID().toString(); - final IdentityProvider idp = buildIdpWithAliasProperties(customZone.getId(), aliasId, IdentityZone.getUaaZoneId()); + final IdentityProvider idp = buildIdpWithAliasProperties(customZone.getId(), aliasId, IdentityZone.getUaaZoneId()); shouldRejectCreation(customZone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } @Test void shouldReject_IdzReferencedInAliasZidDoesNotExist() throws Exception { - final IdentityProvider provider = buildIdpWithAliasProperties( + final IdentityProvider provider = buildIdpWithAliasProperties( IdentityZone.getUaaZoneId(), null, UUID.randomUUID().toString() // does not exist @@ -175,7 +175,7 @@ private void shouldReject_IdpWithOriginAlreadyExistsInAliasZone(final IdentityZo final String originKey = RandomStringUtils.randomAlphabetic(10); // create IdP with origin key in custom zone - final IdentityProvider createdIdp1 = createIdp( + final IdentityProvider createdIdp1 = createIdp( zone1, buildIdpWithAliasProperties(zone1.getId(), null, null, originKey) ); @@ -189,14 +189,14 @@ private void shouldReject_IdpWithOriginAlreadyExistsInAliasZone(final IdentityZo ); } - private void shouldRejectCreation(final IdentityZone zone, final IdentityProvider idp, final HttpStatus expectedStatus) throws Exception { + private void shouldRejectCreation(final IdentityZone zone, final IdentityProvider idp, final HttpStatus expectedStatus) throws Exception { assertThat(expectedStatus.isError()).isTrue(); final MvcResult result = createIdpAndReturnResult(zone, idp); assertThat(result.getResponse().getStatus()).isEqualTo(expectedStatus.value()); // after the failed creation, the IdP must not exist - final List idpsInZoneAfterFailedCreation = readAllIdpsInZone(zone); + final List> idpsInZoneAfterFailedCreation = readAllIdpsInZone(zone); assertThat(idpsInZoneAfterFailedCreation.stream().map(IdentityProvider::getOriginKey).collect(toSet())) .doesNotContain(idp.getOriginKey()); } @@ -216,7 +216,7 @@ void shouldAccept_ShouldCreateMirroredIdp_CustomToUaaZone() throws Exception { private void shouldAccept_ShouldCreateMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { // create regular idp without alias properties in UAA zone - final IdentityProvider existingIdpWithoutAlias = createIdp( + final IdentityProvider existingIdpWithoutAlias = createIdp( zone1, buildIdpWithAliasProperties(zone1.getId(), null, null) ); @@ -225,16 +225,16 @@ private void shouldAccept_ShouldCreateMirroredIdp(final IdentityZone zone1, fina // perform update: set Alias ZID existingIdpWithoutAlias.setAliasZid(zone2.getId()); - final IdentityProvider idpAfterUpdate = updateIdp(zone1, existingIdpWithoutAlias); + final IdentityProvider idpAfterUpdate = updateIdp(zone1, existingIdpWithoutAlias); assertThat(idpAfterUpdate.getAliasId()).isNotBlank(); assertThat(idpAfterUpdate.getAliasZid()).isNotBlank(); assertThat(zone2.getId()).isEqualTo(idpAfterUpdate.getAliasZid()); // read mirrored IdP through alias id in original IdP final String id = idpAfterUpdate.getAliasId(); - final Optional idp = readIdpFromZoneIfExists(zone2.getId(), id); + final Optional> idp = readIdpFromZoneIfExists(zone2.getId(), id); assertThat(idp).isPresent(); - final IdentityProvider mirroredIdp = idp.get(); + final IdentityProvider mirroredIdp = idp.get(); assertIdpReferencesOtherIdp(mirroredIdp, idpAfterUpdate); assertOtherPropertiesAreEqual(idpAfterUpdate, mirroredIdp); } @@ -251,12 +251,12 @@ void shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged_CustomToUaaZone( private void shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged(final IdentityZone zone1, final IdentityZone zone2) throws Exception { // create a mirrored IdP - final IdentityProvider originalIdp = createMirroredIdp(zone1, zone2); + final IdentityProvider originalIdp = createMirroredIdp(zone1, zone2); // update other property final String newName = "new name"; originalIdp.setName(newName); - final IdentityProvider updatedOriginalIdp = updateIdp(zone1, originalIdp); + final IdentityProvider updatedOriginalIdp = updateIdp(zone1, originalIdp); assertThat(updatedOriginalIdp).isNotNull(); assertThat(updatedOriginalIdp.getAliasId()).isNotBlank(); assertThat(updatedOriginalIdp.getAliasZid()).isNotBlank(); @@ -265,7 +265,7 @@ private void shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged(final Id // check if the change is propagated to the mirrored IdP final String id = updatedOriginalIdp.getAliasId(); - final Optional mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id); + final Optional> mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id); assertThat(mirroredIdp).isPresent(); assertIdpReferencesOtherIdp(mirroredIdp.get(), updatedOriginalIdp); assertThat(mirroredIdp.get().getName()).isNotBlank().isEqualTo(newName); @@ -282,7 +282,7 @@ void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp_CustomToUa } private void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final IdentityProvider idp = createMirroredIdp(zone1, zone2); + final IdentityProvider idp = createMirroredIdp(zone1, zone2); // delete the mirrored IdP directly in the DB -> after that, there is a dangling reference final JdbcIdentityProviderProvisioning identityProviderProvisioning = webApplicationContext.getBean(JdbcIdentityProviderProvisioning.class); @@ -291,13 +291,13 @@ private void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp(fi // update some other property on the original IdP idp.setName("some-new-name"); - final IdentityProvider updatedIdp = updateIdp(zone1, idp); + final IdentityProvider updatedIdp = updateIdp(zone1, idp); assertThat(updatedIdp.getAliasId()).isNotBlank().isNotEqualTo(idp.getAliasId()); assertThat(updatedIdp.getAliasZid()).isNotBlank().isEqualTo(idp.getAliasZid()); // check if the new mirrored IdP is present and has the correct properties final String id = updatedIdp.getAliasId(); - final Optional mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id); + final Optional> mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id); assertThat(mirroredIdp).isPresent(); assertIdpReferencesOtherIdp(updatedIdp, mirroredIdp.get()); assertOtherPropertiesAreEqual(updatedIdp, mirroredIdp.get()); @@ -321,7 +321,7 @@ private void shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp( final IdentityZone zone1, final IdentityZone zone2 ) throws Exception { - final IdentityProvider originalIdp = createMirroredIdp(zone1, zone2); + final IdentityProvider originalIdp = createMirroredIdp(zone1, zone2); originalIdp.setAliasId(newAliasId); originalIdp.setAliasZid(newAliasZid); shouldRejectUpdate(zone1, originalIdp, HttpStatus.UNPROCESSABLE_ENTITY); @@ -345,8 +345,8 @@ void shouldReject_OnlyAliasIdSet_CustomZone() throws Exception { } private void shouldReject_OnlyAliasIdSet(final IdentityZone zone) throws Exception { - final IdentityProvider idp = buildIdpWithAliasProperties(zone.getId(), null, null); - final IdentityProvider createdProvider = createIdp(zone, idp); + final IdentityProvider idp = buildIdpWithAliasProperties(zone.getId(), null, null); + final IdentityProvider createdProvider = createIdp(zone, idp); assertThat(createdProvider.getAliasZid()).isBlank(); createdProvider.setAliasId(UUID.randomUUID().toString()); shouldRejectUpdate(zone, createdProvider, HttpStatus.UNPROCESSABLE_ENTITY); @@ -366,7 +366,7 @@ private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final Identi final String originKey = RandomStringUtils.randomAlphabetic(10); // create IdP with origin key in zone 2 - final IdentityProvider existingIdpInZone2 = buildIdpWithAliasProperties( + final IdentityProvider existingIdpInZone2 = buildIdpWithAliasProperties( zone2.getId(), null, null, @@ -375,13 +375,13 @@ private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final Identi createIdp(zone2, existingIdpInZone2); // create IdP with same origin key in zone 1 - final IdentityProvider idp = buildIdpWithAliasProperties( + final IdentityProvider idp = buildIdpWithAliasProperties( zone1.getId(), null, null, originKey // same origin key ); - final IdentityProvider providerInZone1 = createIdp(zone1, idp); + final IdentityProvider providerInZone1 = createIdp(zone1, idp); // update the alias ZID to zone 2, where an IdP with this origin already exists -> should fail providerInZone1.setAliasZid(zone2.getId()); @@ -390,7 +390,7 @@ private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final Identi @Test void shouldReject_IdpInCustomZoneMirroredToOtherCustomZone() throws Exception { - final IdentityProvider idpInCustomZone = createIdp( + final IdentityProvider idpInCustomZone = createIdp( customZone, buildIdpWithAliasProperties(customZone.getId(), null, null) ); @@ -411,7 +411,7 @@ void shouldReject_AliasZidSetToSameZone_CustomZone() throws Exception { } private void shouldReject_AliasZidSetToSameZone(final IdentityZone zone) throws Exception { - final IdentityProvider idp = createIdp( + final IdentityProvider idp = createIdp( zone, buildIdpWithAliasProperties(zone.getId(), null, null) ); @@ -419,12 +419,12 @@ private void shouldReject_AliasZidSetToSameZone(final IdentityZone zone) throws shouldRejectUpdate(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } - private IdentityProvider updateIdp(final IdentityZone zone, final IdentityProvider updatePayload) throws Exception { + private IdentityProvider updateIdp(final IdentityZone zone, final IdentityProvider updatePayload) throws Exception { updatePayload.setIdentityZoneId(zone.getId()); final MvcResult result = updateIdpAndReturnResult(zone, updatePayload); assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); - final IdentityProvider originalIdpAfterUpdate = JsonUtils.readValue( + final IdentityProvider originalIdpAfterUpdate = JsonUtils.readValue( result.getResponse().getContentAsString(), IdentityProvider.class ); @@ -434,7 +434,7 @@ private IdentityProvider updateIdp(final IdentityZone zone, final IdentityProvid return originalIdpAfterUpdate; } - private MvcResult updateIdpAndReturnResult(final IdentityZone zone, final IdentityProvider updatePayload) throws Exception { + private MvcResult updateIdpAndReturnResult(final IdentityZone zone, final IdentityProvider updatePayload) throws Exception { final String id = updatePayload.getId(); assertThat(id).isNotNull().isNotBlank(); @@ -446,20 +446,20 @@ private MvcResult updateIdpAndReturnResult(final IdentityZone zone, final Identi return mockMvc.perform(updateRequestBuilder).andReturn(); } - private void shouldRejectUpdate(final IdentityZone zone, final IdentityProvider idp, final HttpStatus expectedErrorStatus) throws Exception { + private void shouldRejectUpdate(final IdentityZone zone, final IdentityProvider idp, final HttpStatus expectedErrorStatus) throws Exception { assertThat(idp.getId()).isNotBlank(); assertThat(expectedErrorStatus.isError()).isTrue(); // read existing IdP before update final String id = idp.getId(); - final Optional idpBeforeUpdateOpt = readIdpFromZoneIfExists(zone.getId(), id); + final Optional> idpBeforeUpdateOpt = readIdpFromZoneIfExists(zone.getId(), id); assertThat(idpBeforeUpdateOpt).isPresent(); - final IdentityProvider idpBeforeUpdate = idpBeforeUpdateOpt.get(); + final IdentityProvider idpBeforeUpdate = idpBeforeUpdateOpt.get(); // if alias properties set: read mirrored IdP before update - final IdentityProvider mirroredIdpBeforeUpdate; + final IdentityProvider mirroredIdpBeforeUpdate; if (hasText(idpBeforeUpdate.getAliasId()) && hasText(idpBeforeUpdate.getAliasZid())) { - final Optional mirroredIdpBeforeUpdateOpt = readIdpFromZoneIfExists( + final Optional> mirroredIdpBeforeUpdateOpt = readIdpFromZoneIfExists( idpBeforeUpdate.getAliasZid(), idpBeforeUpdate.getAliasId() ); @@ -474,7 +474,7 @@ private void shouldRejectUpdate(final IdentityZone zone, final IdentityProvider assertThat(result.getResponse().getStatus()).isEqualTo(expectedErrorStatus.value()); // read again: original IdP should remain unchanged - final Optional idpAfterFailedUpdateOpt = readIdpFromZoneIfExists( + final Optional> idpAfterFailedUpdateOpt = readIdpFromZoneIfExists( zone.getId(), idp.getId() ); @@ -482,7 +482,7 @@ private void shouldRejectUpdate(final IdentityZone zone, final IdentityProvider // if a mirrored IdP was present before update, check if it also remains unchanged if (mirroredIdpBeforeUpdate != null) { - final Optional mirroredIdpAfterFailedUpdateOpt = readIdpFromZoneIfExists( + final Optional> mirroredIdpAfterFailedUpdateOpt = readIdpFromZoneIfExists( idpBeforeUpdate.getAliasZid(), idpBeforeUpdate.getAliasId() ); @@ -504,7 +504,7 @@ void shouldDeleteMirroredIdp_CustomToUaaZone() throws Exception { } private void shouldDeleteMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final IdentityProvider idpInZone1 = createMirroredIdp(zone1, zone2); + final IdentityProvider idpInZone1 = createMirroredIdp(zone1, zone2); final String id = idpInZone1.getId(); assertThat(id).isNotBlank(); final String aliasId = idpInZone1.getAliasId(); @@ -513,7 +513,7 @@ private void shouldDeleteMirroredIdp(final IdentityZone zone1, final IdentityZon assertThat(aliasZid).isNotBlank().isEqualTo(zone2.getId()); // check if mirrored IdP is available in zone 2 - final Optional mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), aliasId); + final Optional> mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), aliasId); assertThat(mirroredIdp).isPresent(); assertThat(mirroredIdp.get().getAliasId()).isNotBlank().isEqualTo(id); assertThat(mirroredIdp.get().getAliasZid()).isNotBlank().isEqualTo(idpInZone1.getIdentityZoneId()); @@ -528,7 +528,7 @@ private void shouldDeleteMirroredIdp(final IdentityZone zone1, final IdentityZon assertThat(response.getResponse().getStatus()).isEqualTo(200); // check if IdP is no longer available in zone 2 - final Optional mirroredIdpAfterDeletionOfOriginalIdp = readIdpFromZoneIfExists(zone2.getId(), aliasId); + final Optional> mirroredIdpAfterDeletionOfOriginalIdp = readIdpFromZoneIfExists(zone2.getId(), aliasId); assertThat(mirroredIdpAfterDeletionOfOriginalIdp).isNotPresent(); } } @@ -563,7 +563,7 @@ private void shouldAccept_MirroredIdpShouldAlsoBeUpdated(final IdentityZone zone idp.setConfig(new UaaIdentityProviderDefinition(passwordPolicy, null)); idp.setAliasZid(zone2.getId()); final String accessTokenForZone1 = getAccessTokenForZone(zone1.getId()); - final IdentityProvider createdIdp = createIdp(zone1, idp); + final IdentityProvider createdIdp = createIdp(zone1, idp); final Date timestampBeforeUpdate = getPasswordNewerThanTimestamp(createdIdp); assertThat(timestampBeforeUpdate).isNotNull(); @@ -579,32 +579,32 @@ private void shouldAccept_MirroredIdpShouldAlsoBeUpdated(final IdentityZone zone // check if timestamp is updated in zone 1 final String id1 = createdIdp.getId(); - final Optional idpInZone1 = readIdpFromZoneIfExists(zone1.getId(), id1); + final Optional> idpInZone1 = readIdpFromZoneIfExists(zone1.getId(), id1); assertThat(idpInZone1).isPresent(); final Date timestampAfterUpdate = getPasswordNewerThanTimestamp(idpInZone1.get()); assertThat(timestampAfterUpdate).isAfter(timestampBeforeUpdate); // check if timestamp is updated in zone 2 final String id = createdIdp.getAliasId(); - final Optional idpInZone2 = readIdpFromZoneIfExists(zone2.getId(), id); + final Optional> idpInZone2 = readIdpFromZoneIfExists(zone2.getId(), id); assertThat(idpInZone2).isPresent(); final Date timestampAfterUpdateMirroredIdp = getPasswordNewerThanTimestamp(idpInZone2.get()); assertThat(timestampAfterUpdateMirroredIdp).isEqualTo(timestampAfterUpdate); } - private Date getPasswordNewerThanTimestamp(final IdentityProvider idp) { + private Date getPasswordNewerThanTimestamp(final IdentityProvider idp) { return ((UaaIdentityProviderDefinition) idp.getConfig()).getPasswordPolicy().getPasswordNewerThan(); } } - private void assertIdpReferencesOtherIdp(final IdentityProvider idp, final IdentityProvider referencedIdp) { + private void assertIdpReferencesOtherIdp(final IdentityProvider idp, final IdentityProvider referencedIdp) { assertThat(idp).isNotNull(); assertThat(referencedIdp).isNotNull(); assertThat(referencedIdp.getId()).isNotBlank().isEqualTo(idp.getAliasId()); assertThat(referencedIdp.getIdentityZoneId()).isNotBlank().isEqualTo(idp.getAliasZid()); } - private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final IdentityProvider mirroredIdp) { + private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final IdentityProvider mirroredIdp) { // apart from the zone ID, the configs should be identical final SamlIdentityProviderDefinition originalIdpConfig = (SamlIdentityProviderDefinition) idp.getConfig(); originalIdpConfig.setZoneId(null); @@ -618,18 +618,18 @@ private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final Ide assertThat(mirroredIdp.getType()).isEqualTo(idp.getType()); } - private IdentityProvider createMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final IdentityProvider provider = buildIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); + private IdentityProvider createMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + final IdentityProvider provider = buildIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); return createIdp(zone1, provider); } - private IdentityProvider createIdp(final IdentityZone zone, final IdentityProvider idp) throws Exception { + private IdentityProvider createIdp(final IdentityZone zone, final IdentityProvider idp) throws Exception { final MvcResult createResult = createIdpAndReturnResult(zone, idp); assertThat(createResult.getResponse().getStatus()).isEqualTo(HttpStatus.CREATED.value()); return JsonUtils.readValue(createResult.getResponse().getContentAsString(), IdentityProvider.class); } - private MvcResult createIdpAndReturnResult(final IdentityZone zone, final IdentityProvider idp) throws Exception { + private MvcResult createIdpAndReturnResult(final IdentityZone zone, final IdentityProvider idp) throws Exception { final MockHttpServletRequestBuilder createRequestBuilder = post("/identity-providers") .param("rawConfig", "true") .header("Authorization", "Bearer " + getAccessTokenForZone(zone.getId())) @@ -678,7 +678,7 @@ private String getAccessTokenForZone(final String zoneId) throws Exception { return accessToken; } - private Optional readIdpFromZoneIfExists(final String zoneId, final String id) throws Exception { + private Optional> readIdpFromZoneIfExists(final String zoneId, final String id) throws Exception { final MockHttpServletRequestBuilder getRequestBuilder = get("/identity-providers/" + id) .param("rawConfig", "true") .header(IdentityZoneSwitchingFilter.HEADER, zoneId) @@ -691,7 +691,7 @@ private Optional readIdpFromZoneIfExists(final String zoneId, case 404: return Optional.empty(); case 200: - final IdentityProvider responseBody = JsonUtils.readValue( + final IdentityProvider responseBody = JsonUtils.readValue( getResult.getResponse().getContentAsString(), IdentityProvider.class ); @@ -702,7 +702,7 @@ private Optional readIdpFromZoneIfExists(final String zoneId, } } - private List readAllIdpsInZone(final IdentityZone zone) throws Exception { + private List> readAllIdpsInZone(final IdentityZone zone) throws Exception { final MockHttpServletRequestBuilder getRequestBuilder = get("/identity-providers") .param("rawConfig", "true") .header(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER, zone.getSubdomain()) @@ -716,12 +716,12 @@ private static List getScopesForZone(final String zoneId, final String.. return Stream.of(scopes).map(scope -> String.format("zones.%s.%s", zoneId, scope)).collect(toList()); } - private static IdentityProvider buildIdpWithAliasProperties(final String idzId, final String aliasId, final String aliasZid) { + private static IdentityProvider buildIdpWithAliasProperties(final String idzId, final String aliasId, final String aliasZid) { final String originKey = RandomStringUtils.randomAlphabetic(8); return buildIdpWithAliasProperties(idzId, aliasId, aliasZid, originKey); } - private static IdentityProvider buildIdpWithAliasProperties(final String idzId, final String aliasId, final String aliasZid, final String originKey) { + private static IdentityProvider buildIdpWithAliasProperties(final String idzId, final String aliasId, final String aliasZid, final String originKey) { final String metadata = String.format( BootstrapSamlIdentityProviderDataTests.xmlWithoutID, "http://localhost:9999/metadata/" + originKey From 49e025f19756cd471cd4539e5a2916ab147680a2 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 21 Dec 2023 13:51:50 +0100 Subject: [PATCH 31/91] Fix Sonar code smells --- .../provider/IdentityProviderEndpoints.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 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 fd4e34b8725..8de936ca638 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 @@ -141,10 +141,10 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden } // persist IdP and mirror if necessary - final IdentityProvider createdIdp; + final IdentityProvider createdIdp; try { createdIdp = transactionTemplate.execute(txStatus -> { - final IdentityProvider createdOriginalIdp = identityProviderProvisioning.create(body, zoneId); + final IdentityProvider createdOriginalIdp = identityProviderProvisioning.create(body, zoneId); createdOriginalIdp.setSerializeConfigRaw(rawConfig); redactSensitiveData(createdOriginalIdp); @@ -164,13 +164,13 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden @Transactional public ResponseEntity deleteIdentityProvider(@PathVariable String id, @RequestParam(required = false, defaultValue = "false") boolean rawConfig) { String identityZoneId = identityZoneManager.getCurrentIdentityZoneId(); - IdentityProvider existing = identityProviderProvisioning.retrieve(id, identityZoneId); + IdentityProvider existing = identityProviderProvisioning.retrieve(id, identityZoneId); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // delete mirrored IdP if alias fields are set if (existing != null && hasText(existing.getAliasZid()) && hasText(existing.getAliasId())) { - IdentityProvider mirroredIdp = identityProviderProvisioning.retrieve(existing.getAliasId(), existing.getAliasZid()); + IdentityProvider mirroredIdp = identityProviderProvisioning.retrieve(existing.getAliasId(), existing.getAliasZid()); mirroredIdp.setSerializeConfigRaw(rawConfig); publisher.publishEvent(new EntityDeletedEvent<>(mirroredIdp, authentication, identityZoneId)); } @@ -201,7 +201,11 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str } if (!aliasPropertiesAreValid(body, existing)) { - logger.error("IdentityProvider[origin="+body.getOriginKey()+"; zone="+body.getIdentityZoneId()+"] - Alias ID and/or ZID changed during update of already mirrored IdP."); + logger.error( + "IdentityProvider[origin={}; zone={}] - Alias ID and/or ZID changed during update of already mirrored IdP.", + body.getOriginKey(), + body.getIdentityZoneId() + ); return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } @@ -219,7 +223,11 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str return ensureConsistencyOfMirroredIdp(updatedOriginalIdp); }); if (updatedIdp == null) { - logger.error("IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "] - Transaction updating IdP and mirrored IdP was not successful, but no exception was thrown."); + logger.error( + "IdentityProvider[origin={}; zone={}] - Transaction updating IdP and mirrored IdP was not successful, but no exception was thrown.", + body.getOriginKey(), + body.getIdentityZoneId() + ); return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } updatedIdp.setSerializeConfigRaw(rawConfig); @@ -250,7 +258,7 @@ public ResponseEntity updateIdentityProviderStatus(@Path uaaIdentityProviderDefinition.getPasswordPolicy().setPasswordNewerThan(passwordNewerThanTimestamp); // update the property in the mirrored IdP if present - final IdentityProvider mirroredIdp; + final IdentityProvider mirroredIdp; if (hasText(existing.getAliasZid()) && hasText(existing.getAliasId())) { mirroredIdp = identityProviderProvisioning.retrieve(existing.getAliasId(), existing.getAliasZid()); final UaaIdentityProviderDefinition definitionMirroredIdp = ObjectUtils.castInstance( @@ -270,7 +278,7 @@ public ResponseEntity updateIdentityProviderStatus(@Path } }); - logger.info("PasswordChangeRequired property set for Identity Provider: " + existing.getId()); + logger.info("PasswordChangeRequired property set for Identity Provider: {}", existing.getId()); return new ResponseEntity<>(body, OK); } From cea991d622b49017ab924c1063f15462996af149 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 21 Dec 2023 15:04:06 +0100 Subject: [PATCH 32/91] Add unit test for deletion of an IdP with a mirrored IdP --- .../provider/IdentityProviderEndpoints.java | 19 ++++---- .../IdentityProviderEndpointsTest.java | 46 +++++++++++++++++++ 2 files changed, 56 insertions(+), 9 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 8de936ca638..654888a9a91 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 @@ -168,21 +168,22 @@ public ResponseEntity deleteIdentityProvider(@PathVariable Str Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (publisher == null || existing == null) { + return new ResponseEntity<>(UNPROCESSABLE_ENTITY); + } + + existing.setSerializeConfigRaw(rawConfig); + publisher.publishEvent(new EntityDeletedEvent<>(existing, authentication, identityZoneId)); + redactSensitiveData(existing); + // delete mirrored IdP if alias fields are set - if (existing != null && hasText(existing.getAliasZid()) && hasText(existing.getAliasId())) { + if (hasText(existing.getAliasZid()) && hasText(existing.getAliasId())) { IdentityProvider mirroredIdp = identityProviderProvisioning.retrieve(existing.getAliasId(), existing.getAliasZid()); mirroredIdp.setSerializeConfigRaw(rawConfig); publisher.publishEvent(new EntityDeletedEvent<>(mirroredIdp, authentication, identityZoneId)); } - if (publisher!=null && existing!=null) { - existing.setSerializeConfigRaw(rawConfig); - publisher.publishEvent(new EntityDeletedEvent<>(existing, authentication, identityZoneId)); - redactSensitiveData(existing); - return new ResponseEntity<>(existing, OK); - } else { - return new ResponseEntity<>(UNPROCESSABLE_ENTITY); - } + return new ResponseEntity<>(existing, OK); } @RequestMapping(value = "{id}", method = PUT) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index c5a82711fb7..62f0f6dae9b 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -1,5 +1,7 @@ package org.cloudfoundry.identity.uaa.provider; +import org.assertj.core.api.Assertions; +import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent; import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.extensions.PollutionPreventionExtension; import org.cloudfoundry.identity.uaa.zone.IdentityZone; @@ -9,6 +11,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; @@ -30,6 +33,7 @@ import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LDAP; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OIDC10; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UNKNOWN; import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.USER_NAME_ATTRIBUTE_NAME; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -41,6 +45,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -445,6 +450,47 @@ void testDeleteIdentityProviderExisting() { assertEquals(validIDP, deleteResponse.getBody()); } + @Test + void testDeleteIdentityProviderMirrored() { + final String idpId = UUID.randomUUID().toString(); + final String mirroredIdpId = UUID.randomUUID().toString(); + final String customZoneId = UUID.randomUUID().toString(); + + final IdentityProvider idp = new IdentityProvider<>(); + idp.setType(OIDC10); + idp.setId(idpId); + idp.setIdentityZoneId(UAA); + idp.setAliasId(mirroredIdpId); + idp.setAliasZid(customZoneId); + when(mockIdentityProviderProvisioning.retrieve(idpId, UAA)).thenReturn(idp); + + final IdentityProvider mirroredIdp = new IdentityProvider<>(); + mirroredIdp.setType(OIDC10); + mirroredIdp.setId(mirroredIdpId); + mirroredIdp.setIdentityZoneId(customZoneId); + mirroredIdp.setAliasId(idpId); + mirroredIdp.setAliasZid(UAA); + when(mockIdentityProviderProvisioning.retrieve(mirroredIdpId, customZoneId)).thenReturn(mirroredIdp); + + final ApplicationEventPublisher mockEventPublisher = mock(ApplicationEventPublisher.class); + identityProviderEndpoints.setApplicationEventPublisher(mockEventPublisher); + doNothing().when(mockEventPublisher).publishEvent(any()); + + identityProviderEndpoints.deleteIdentityProvider(idpId, true); + final ArgumentCaptor> entityDeletedEventCaptor = ArgumentCaptor.forClass(EntityDeletedEvent.class); + verify(mockEventPublisher, times(2)).publishEvent(entityDeletedEventCaptor.capture()); + + final EntityDeletedEvent firstEvent = entityDeletedEventCaptor.getAllValues().get(0); + Assertions.assertThat(firstEvent).isNotNull(); + Assertions.assertThat(firstEvent.getIdentityZoneId()).isEqualTo(UAA); + Assertions.assertThat(((IdentityProvider) firstEvent.getSource()).getId()).isEqualTo(idpId); + + final EntityDeletedEvent secondEvent = entityDeletedEventCaptor.getAllValues().get(1); + Assertions.assertThat(secondEvent).isNotNull(); + Assertions.assertThat(secondEvent.getIdentityZoneId()).isEqualTo(UAA); + Assertions.assertThat(((IdentityProvider) secondEvent.getSource()).getId()).isEqualTo(mirroredIdpId); + } + @Test void testDeleteIdentityProviderNotExisting() { String zoneId = IdentityZone.getUaaZoneId(); From 7c1c14686deddc2bbf2c10c64426be8fd36aa11a Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 21 Dec 2023 15:44:44 +0100 Subject: [PATCH 33/91] Add unit test for invalid alias properties during IdP creation --- .../IdentityProviderEndpointsTest.java | 97 +++++++++++++------ 1 file changed, 67 insertions(+), 30 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index 62f0f6dae9b..9bcc1299849 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -1,35 +1,5 @@ package org.cloudfoundry.identity.uaa.provider; -import org.assertj.core.api.Assertions; -import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent; -import org.cloudfoundry.identity.uaa.constants.OriginKeys; -import org.cloudfoundry.identity.uaa.extensions.PollutionPreventionExtension; -import org.cloudfoundry.identity.uaa.zone.IdentityZone; -import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; -import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.transaction.PlatformTransactionManager; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; -import java.util.function.Consumer; -import java.util.function.Supplier; - import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LDAP; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OIDC10; @@ -54,6 +24,37 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.assertj.core.api.Assertions; +import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent; +import org.cloudfoundry.identity.uaa.constants.OriginKeys; +import org.cloudfoundry.identity.uaa.extensions.PollutionPreventionExtension; +import org.cloudfoundry.identity.uaa.zone.IdentityZone; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; +import org.cloudfoundry.identity.uaa.zone.ZoneDoesNotExistsException; +import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.PlatformTransactionManager; + @ExtendWith(PollutionPreventionExtension.class) @ExtendWith(MockitoExtension.class) class IdentityProviderEndpointsTest { @@ -352,6 +353,42 @@ void create_ldap_provider_removes_password() throws Exception { assertNull(((LdapIdentityProviderDefinition) created.getConfig()).getBindPassword()); } + @Test + void testCreateIdentityProvider_AliasPropertiesInvalid() throws MetadataProviderException { + // (1) aliasId is not empty + IdentityProvider idp = getExternalOAuthProvider(); + idp.setAliasId(UUID.randomUUID().toString()); + ResponseEntity response = identityProviderEndpoints.createIdentityProvider(idp, true); + Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + + // (2) aliasZid set, but referenced zone does not exist + idp = getExternalOAuthProvider(); + final String notExistingZoneId = UUID.randomUUID().toString(); + idp.setAliasZid(notExistingZoneId); + when(mockIdentityZoneProvisioning.retrieve(notExistingZoneId)).thenThrow(ZoneDoesNotExistsException.class); + response = identityProviderEndpoints.createIdentityProvider(idp, true); + Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + + // (3) aliasZid and IdZ equal + idp = getExternalOAuthProvider(); + idp.setAliasZid(idp.getIdentityZoneId()); + response = identityProviderEndpoints.createIdentityProvider(idp, true); + Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + + // (4) neither IdZ nor aliasZid are "uaa" + idp = getExternalOAuthProvider(); + final String zoneId1 = UUID.randomUUID().toString(); + when(mockIdentityZoneManager.getCurrentIdentityZoneId()).thenReturn(zoneId1); + final String zoneId2 = UUID.randomUUID().toString(); + final IdentityZone zone2 = new IdentityZone(); + zone2.setId(zoneId2); + when(mockIdentityZoneProvisioning.retrieve(zoneId2)).thenReturn(zone2); + idp.setIdentityZoneId(zoneId1); + idp.setAliasZid(zoneId2); + response = identityProviderEndpoints.createIdentityProvider(idp, true); + Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + } + @Test void create_oauth_provider_removes_password() throws Exception { String zoneId = IdentityZone.getUaaZoneId(); From b2ec24d2b5c4f60ae0dd19ef5076b26542819bb8 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 21 Dec 2023 16:35:47 +0100 Subject: [PATCH 34/91] Add unit test for invalid alias properties during update of already mirrored IdP --- .../IdentityProviderEndpointsTest.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index 9bcc1299849..fc3a6ae43a4 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -338,6 +338,55 @@ void update_ldap_provider_takes_new_password() throws Exception { assertNull(((LdapIdentityProviderDefinition) response.getBody().getConfig()).getBindPassword()); } + @Test + void testUpdateIdentityProvider_AlreadyMirrored_InvalidAliasPropertyChange() throws MetadataProviderException { + final String existingIdpId = UUID.randomUUID().toString(); + final String customZoneId = UUID.randomUUID().toString(); + final String mirroredIdpId = UUID.randomUUID().toString(); + + final Supplier> existingIdpProvider = () -> { + final IdentityProvider idp = getExternalOAuthProvider(); + idp.setId(existingIdpId); + idp.setAliasZid(customZoneId); + idp.setAliasId(mirroredIdpId); + return idp; + }; + + // original IdP with reference to a mirrored IdP + final IdentityProvider existingIdp = existingIdpProvider.get(); + when(mockIdentityProviderProvisioning.retrieve(existingIdpId, IdentityZone.getUaaZoneId())) + .thenReturn(existingIdp); + + // (1) aliasId removed + IdentityProvider requestBody = existingIdpProvider.get(); + requestBody.setAliasId(""); + ResponseEntity response = identityProviderEndpoints.updateIdentityProvider(existingIdpId, requestBody, true); + Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + + // (2) aliasId changed + requestBody = existingIdpProvider.get(); + requestBody.setAliasId(UUID.randomUUID().toString()); + response = identityProviderEndpoints.updateIdentityProvider(existingIdpId, requestBody, true); + Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + + // (3) aliasZid removed + requestBody = existingIdpProvider.get(); + requestBody.setAliasZid(""); + response = identityProviderEndpoints.updateIdentityProvider(existingIdpId, requestBody, true); + Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + + // (4) aliasZid changed + requestBody = existingIdpProvider.get(); + requestBody.setAliasZid(UUID.randomUUID().toString()); + response = identityProviderEndpoints.updateIdentityProvider(existingIdpId, requestBody, true); + Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + } + + @Test + void testUpdateIdentityProvider_NotYetMirrored_InvalidAliasPropertyChange() { + + } + @Test void create_ldap_provider_removes_password() throws Exception { String zoneId = IdentityZone.getUaaZoneId(); From c239ebd68910c290852a964b4253cb497efa9f26 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 21 Dec 2023 16:43:47 +0100 Subject: [PATCH 35/91] Change log level from error to warn --- .../identity/uaa/provider/IdentityProviderEndpoints.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 654888a9a91..fb270d27fae 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 @@ -153,7 +153,7 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden } catch (final IdpAlreadyExistsException e) { return new ResponseEntity<>(body, CONFLICT); } catch (final Exception e) { - logger.error("Unable to create IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "]", e); + logger.warn("Unable to create IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "]", e); return new ResponseEntity<>(body, INTERNAL_SERVER_ERROR); } @@ -202,7 +202,7 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str } if (!aliasPropertiesAreValid(body, existing)) { - logger.error( + logger.warn( "IdentityProvider[origin={}; zone={}] - Alias ID and/or ZID changed during update of already mirrored IdP.", body.getOriginKey(), body.getIdentityZoneId() @@ -224,7 +224,7 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str return ensureConsistencyOfMirroredIdp(updatedOriginalIdp); }); if (updatedIdp == null) { - logger.error( + logger.warn( "IdentityProvider[origin={}; zone={}] - Transaction updating IdP and mirrored IdP was not successful, but no exception was thrown.", body.getOriginKey(), body.getIdentityZoneId() @@ -326,7 +326,7 @@ public ResponseEntity testIdentityProvider(@RequestBody IdentityProvider status = BAD_REQUEST; exception = getExceptionString(x); } catch (Exception x) { - logger.error("Identity provider validation failed.", x); + logger.warn("Identity provider validation failed.", x); status = INTERNAL_SERVER_ERROR; exception = "check server logs"; }finally { From f6688889815590ad4e418435a4c826271fdfbed1 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 21 Dec 2023 16:44:48 +0100 Subject: [PATCH 36/91] Remove empty test case --- .../identity/uaa/provider/IdentityProviderEndpointsTest.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index fc3a6ae43a4..5aecac20f4b 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -382,11 +382,6 @@ void testUpdateIdentityProvider_AlreadyMirrored_InvalidAliasPropertyChange() thr Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); } - @Test - void testUpdateIdentityProvider_NotYetMirrored_InvalidAliasPropertyChange() { - - } - @Test void create_ldap_provider_removes_password() throws Exception { String zoneId = IdentityZone.getUaaZoneId(); From eae8d841df1e5b5a2e94b60ca4d819278f23a71a Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 21 Dec 2023 16:59:51 +0100 Subject: [PATCH 37/91] Fix Sonar security warning --- .../identity/uaa/provider/IdentityProviderEndpoints.java | 5 +++-- 1 file changed, 3 insertions(+), 2 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 fb270d27fae..efdc533ec53 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 @@ -60,6 +60,7 @@ import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OIDC10; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; +import static org.cloudfoundry.identity.uaa.util.UaaStringUtils.getCleanedUserControlString; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.CONFLICT; import static org.springframework.http.HttpStatus.CREATED; @@ -204,8 +205,8 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str if (!aliasPropertiesAreValid(body, existing)) { logger.warn( "IdentityProvider[origin={}; zone={}] - Alias ID and/or ZID changed during update of already mirrored IdP.", - body.getOriginKey(), - body.getIdentityZoneId() + getCleanedUserControlString(body.getOriginKey()), + getCleanedUserControlString(body.getIdentityZoneId()) ); return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } From 780208a24f4ab61f14e57e5788fa45741c8197f4 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 21 Dec 2023 18:36:12 +0100 Subject: [PATCH 38/91] Add unit test for successful mirroring during IdP creation --- .../IdentityProviderEndpointsTest.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index 5aecac20f4b..59e9d08fce2 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -13,6 +13,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doNothing; @@ -29,6 +30,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.UUID; import java.util.function.Consumer; import java.util.function.Supplier; @@ -45,6 +47,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; @@ -433,6 +436,83 @@ void testCreateIdentityProvider_AliasPropertiesInvalid() throws MetadataProvider Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); } + @Test + void testCreateIdentityProvider_ValidAliasProperties() throws MetadataProviderException { + // arrange custom zone exists + final String customZoneId = UUID.randomUUID().toString(); + final IdentityZone customZone = new IdentityZone(); + customZone.setId(customZoneId); + when(mockIdentityZoneProvisioning.retrieve(customZoneId)).thenReturn(customZone); + + final Supplier> requestBodyProvider = () -> { + final IdentityProvider requestBody = getExternalOAuthProvider(); + requestBody.setId(null); + requestBody.setAliasZid(customZoneId); + return requestBody; + }; + + // idpProvisioning.create should return request body with new ID + final IdentityProvider createdOriginalIdp = requestBodyProvider.get(); + final String originalIdpId = UUID.randomUUID().toString(); + createdOriginalIdp.setId(originalIdpId); + final IdpWithAliasMatcher requestBodyMatcher = new IdpWithAliasMatcher(UAA, null, null, customZoneId); + + // idpProvisioning.create should add ID to mirrored IdP + final IdentityProvider persistedMirroredIdp = requestBodyProvider.get(); + final String mirroredIdpId = UUID.randomUUID().toString(); + persistedMirroredIdp.setAliasId(originalIdpId); + persistedMirroredIdp.setAliasZid(UAA); + persistedMirroredIdp.setIdentityZoneId(customZoneId); + persistedMirroredIdp.setId(mirroredIdpId); + final IdpWithAliasMatcher mirroredIdpMatcher = new IdpWithAliasMatcher(customZoneId, null, originalIdpId, UAA); + when(mockIdentityProviderProvisioning.create(any(), anyString())).thenAnswer(invocation -> { + final IdentityProvider idp = invocation.getArgument(0); + final String idzId = invocation.getArgument(1); + if (requestBodyMatcher.matches(idp) && idzId.equals(UAA)) { + return createdOriginalIdp; + } + if (mirroredIdpMatcher.matches(idp) && idzId.equals(customZoneId)) { + return persistedMirroredIdp; + } + return null; + }); + + // mock idpProvisioning.update + final IdentityProvider createdOriginalIdpWithAliasId = requestBodyProvider.get(); + createdOriginalIdpWithAliasId.setId(originalIdpId); + createdOriginalIdpWithAliasId.setAliasId(mirroredIdpId); + when(mockIdentityProviderProvisioning.update( + argThat(new IdpWithAliasMatcher(UAA, originalIdpId, mirroredIdpId, customZoneId)), + eq(UAA) + )).thenReturn(createdOriginalIdpWithAliasId); + + // perform the endpoint call + final IdentityProvider requestBody = requestBodyProvider.get(); + final ResponseEntity response = identityProviderEndpoints.createIdentityProvider(requestBody, true); + Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + Assertions.assertThat(response.getBody()).isEqualTo(createdOriginalIdpWithAliasId); + } + + private static class IdpWithAliasMatcher implements ArgumentMatcher> { + private final String identityZoneId; + private final String id; + private final String aliasId; + private final String aliasZid; + + public IdpWithAliasMatcher(final String identityZoneId, final String id, final String aliasId, final String aliasZid) { + this.identityZoneId = identityZoneId; + this.id = id; + this.aliasId = aliasId; + this.aliasZid = aliasZid; + } + + @Override + public boolean matches(final IdentityProvider argument) { + return Objects.equals(id, argument.getId()) && Objects.equals(identityZoneId, argument.getIdentityZoneId()) + && Objects.equals(aliasId, argument.getAliasId()) && Objects.equals(aliasZid, argument.getAliasZid()); + } + } + @Test void create_oauth_provider_removes_password() throws Exception { String zoneId = IdentityZone.getUaaZoneId(); From 6e3d0a948b14cae56fa41996a0a8d1b9ea0e6a6a Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 21 Dec 2023 18:38:06 +0100 Subject: [PATCH 39/91] Fix Sonar security warning --- .../identity/uaa/provider/IdentityProviderEndpoints.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 efdc533ec53..8f62e336848 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 @@ -227,8 +227,8 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str if (updatedIdp == null) { logger.warn( "IdentityProvider[origin={}; zone={}] - Transaction updating IdP and mirrored IdP was not successful, but no exception was thrown.", - body.getOriginKey(), - body.getIdentityZoneId() + getCleanedUserControlString(body.getOriginKey()), + getCleanedUserControlString(body.getIdentityZoneId()) ); return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } From bc2a4a974222c766bf2eac8049ac2b025332af5e Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 22 Dec 2023 09:36:06 +0100 Subject: [PATCH 40/91] Add unit test for update status call --- .../IdentityProviderEndpointsTest.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index 59e9d08fce2..87c059d2ae8 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -29,6 +29,7 @@ import java.net.URL; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.List; import java.util.Objects; import java.util.UUID; @@ -58,6 +59,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.transaction.PlatformTransactionManager; +import com.sun.mail.imap.protocol.ID; + @ExtendWith(PollutionPreventionExtension.class) @ExtendWith(MockitoExtension.class) class IdentityProviderEndpointsTest { @@ -385,6 +388,61 @@ void testUpdateIdentityProvider_AlreadyMirrored_InvalidAliasPropertyChange() thr Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); } + @Test + void testUpdateStatus_ShouldAlsoUpdateMirroredIdpIfPresent() { + when(mockIdentityZoneManager.getCurrentIdentityZoneId()).thenReturn(UAA); + + final IdentityProvider idp = new IdentityProvider<>(); + final String idpId = UUID.randomUUID().toString(); + idp.setId(idpId); + idp.setIdentityZoneId(UAA); + final String mirroredIdpId = UUID.randomUUID().toString(); + idp.setAliasId(mirroredIdpId); + final String customZoneId = UUID.randomUUID().toString(); + idp.setAliasZid(customZoneId); + final UaaIdentityProviderDefinition config = new UaaIdentityProviderDefinition(); + final PasswordPolicy passwordPolicy = new PasswordPolicy(); + config.setPasswordPolicy(passwordPolicy); + idp.setConfig(config); + when(mockIdentityProviderProvisioning.retrieve(idpId, UAA)).thenReturn(idp); + + final IdentityProvider mirroredIdp = new IdentityProvider<>(); + mirroredIdp.setId(mirroredIdpId); + mirroredIdp.setIdentityZoneId(customZoneId); + mirroredIdp.setAliasId(idpId); + mirroredIdp.setAliasZid(UAA); + mirroredIdp.setConfig(config); + when(mockIdentityProviderProvisioning.retrieve(mirroredIdpId, customZoneId)).thenReturn(mirroredIdp); + + when(mockIdentityProviderProvisioning.update(any(), anyString())).thenReturn(null); + + final Date timestampBeforeUpdate = new Date(System.currentTimeMillis()); + + final IdentityProviderStatus requestBody = new IdentityProviderStatus(); + requestBody.setRequirePasswordChange(true); + final ResponseEntity response = identityProviderEndpoints.updateIdentityProviderStatus(idpId, requestBody); + + Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + Assertions.assertThat(response.getBody()).isNotNull().isEqualTo(requestBody); + + final ArgumentCaptor idpArgumentCaptor = ArgumentCaptor.forClass(IdentityProvider.class); + verify(mockIdentityProviderProvisioning, times(2)).update(idpArgumentCaptor.capture(), anyString()); + + // expecting original IdP with a new timestamp + final IdentityProvider firstIdp = idpArgumentCaptor.getAllValues().get(0); + Assertions.assertThat(firstIdp).isNotNull(); + Assertions.assertThat(firstIdp.getId()).isEqualTo(idpId); + final Date timestampAfterUpdateFirstIdp = ((UaaIdentityProviderDefinition) firstIdp.getConfig()).getPasswordPolicy().getPasswordNewerThan(); + Assertions.assertThat(timestampAfterUpdateFirstIdp).isNotNull().isAfter(timestampBeforeUpdate); + + // expecting mirrored IdP with same timestamp + final IdentityProvider secondIdp = idpArgumentCaptor.getAllValues().get(1); + Assertions.assertThat(secondIdp).isNotNull(); + Assertions.assertThat(secondIdp.getId()).isEqualTo(mirroredIdpId); + final Date timestampAfterUpdateSecondIdp = ((UaaIdentityProviderDefinition) secondIdp.getConfig()).getPasswordPolicy().getPasswordNewerThan(); + Assertions.assertThat(timestampAfterUpdateFirstIdp).isNotNull().isEqualTo(timestampAfterUpdateSecondIdp); + } + @Test void create_ldap_provider_removes_password() throws Exception { String zoneId = IdentityZone.getUaaZoneId(); From 0bfe02c77e09607569e9b05586a295356d6fe272 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 22 Dec 2023 09:58:34 +0100 Subject: [PATCH 41/91] Add unit test for successful update of IdP with mirrored IdP --- .../IdentityProviderEndpointsTest.java | 65 +++++++++++++++++-- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index 87c059d2ae8..97f25721aeb 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -350,7 +350,7 @@ void testUpdateIdentityProvider_AlreadyMirrored_InvalidAliasPropertyChange() thr final String customZoneId = UUID.randomUUID().toString(); final String mirroredIdpId = UUID.randomUUID().toString(); - final Supplier> existingIdpProvider = () -> { + final Supplier> existingIdpSupplier = () -> { final IdentityProvider idp = getExternalOAuthProvider(); idp.setId(existingIdpId); idp.setAliasZid(customZoneId); @@ -359,35 +359,88 @@ void testUpdateIdentityProvider_AlreadyMirrored_InvalidAliasPropertyChange() thr }; // original IdP with reference to a mirrored IdP - final IdentityProvider existingIdp = existingIdpProvider.get(); + final IdentityProvider existingIdp = existingIdpSupplier.get(); when(mockIdentityProviderProvisioning.retrieve(existingIdpId, IdentityZone.getUaaZoneId())) .thenReturn(existingIdp); // (1) aliasId removed - IdentityProvider requestBody = existingIdpProvider.get(); + IdentityProvider requestBody = existingIdpSupplier.get(); requestBody.setAliasId(""); ResponseEntity response = identityProviderEndpoints.updateIdentityProvider(existingIdpId, requestBody, true); Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); // (2) aliasId changed - requestBody = existingIdpProvider.get(); + requestBody = existingIdpSupplier.get(); requestBody.setAliasId(UUID.randomUUID().toString()); response = identityProviderEndpoints.updateIdentityProvider(existingIdpId, requestBody, true); Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); // (3) aliasZid removed - requestBody = existingIdpProvider.get(); + requestBody = existingIdpSupplier.get(); requestBody.setAliasZid(""); response = identityProviderEndpoints.updateIdentityProvider(existingIdpId, requestBody, true); Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); // (4) aliasZid changed - requestBody = existingIdpProvider.get(); + requestBody = existingIdpSupplier.get(); requestBody.setAliasZid(UUID.randomUUID().toString()); response = identityProviderEndpoints.updateIdentityProvider(existingIdpId, requestBody, true); Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); } + @Test + void testUpdateIdentityProvider_AlreadyMirrored_ValidChange() throws MetadataProviderException { + final String existingIdpId = UUID.randomUUID().toString(); + final String customZoneId = UUID.randomUUID().toString(); + final String mirroredIdpId = UUID.randomUUID().toString(); + + when(mockIdentityZoneManager.getCurrentIdentityZoneId()).thenReturn(UAA); + + final Supplier> existingIdpSupplier = () -> { + final IdentityProvider idp = getExternalOAuthProvider(); + idp.setId(existingIdpId); + idp.setAliasZid(customZoneId); + idp.setAliasId(mirroredIdpId); + return idp; + }; + + final IdentityProvider existingIdp = existingIdpSupplier.get(); + when(mockIdentityProviderProvisioning.retrieve(existingIdpId, UAA)).thenReturn(existingIdp); + final IdentityProvider mirroredIdp = getExternalOAuthProvider(); + mirroredIdp.setId(mirroredIdpId); + mirroredIdp.setIdentityZoneId(customZoneId); + mirroredIdp.setAliasId(existingIdp.getId()); + mirroredIdp.setAliasZid(UAA); + when(mockIdentityProviderProvisioning.retrieve(mirroredIdpId, customZoneId)).thenReturn(mirroredIdp); + + when(mockIdentityProviderProvisioning.update(any(), anyString())) + .thenAnswer(invocation -> invocation.getArgument(0)); + + final IdentityProvider requestBody = existingIdpSupplier.get(); + final String newName = "new name"; + requestBody.setName(newName); + final ResponseEntity response = identityProviderEndpoints.updateIdentityProvider(existingIdpId, requestBody, true); + Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + final IdentityProvider responseBody = response.getBody(); + Assertions.assertThat(responseBody).isNotNull(); + Assertions.assertThat(responseBody.getName()).isNotNull().isEqualTo(newName); + + final ArgumentCaptor idpArgumentCaptor = ArgumentCaptor.forClass(IdentityProvider.class); + verify(mockIdentityProviderProvisioning, times(2)).update(idpArgumentCaptor.capture(), anyString()); + + // expecting original IdP with the new name + final IdentityProvider firstIdp = idpArgumentCaptor.getAllValues().get(0); + Assertions.assertThat(firstIdp).isNotNull(); + Assertions.assertThat(firstIdp.getId()).isEqualTo(existingIdpId); + Assertions.assertThat(firstIdp.getName()).isEqualTo(newName); + + // expecting mirrored IdP with the new name + final IdentityProvider secondIdp = idpArgumentCaptor.getAllValues().get(1); + Assertions.assertThat(secondIdp).isNotNull(); + Assertions.assertThat(secondIdp.getId()).isEqualTo(mirroredIdpId); + Assertions.assertThat(secondIdp.getName()).isEqualTo(newName); + } + @Test void testUpdateStatus_ShouldAlsoUpdateMirroredIdpIfPresent() { when(mockIdentityZoneManager.getCurrentIdentityZoneId()).thenReturn(UAA); From 4fe4b045fa83b438eadd48aeece84c89df456cb9 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 22 Dec 2023 10:32:58 +0100 Subject: [PATCH 42/91] Add unit test for IdentityProvider.toString --- .../uaa/provider/IdentityProviderTest.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java diff --git a/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java b/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java new file mode 100644 index 00000000000..a7756b93d44 --- /dev/null +++ b/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java @@ -0,0 +1,33 @@ +package org.cloudfoundry.identity.uaa.provider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class IdentityProviderTest { + + private static IdentityProvider idp; + + @BeforeAll + static void beforeAll() { + idp = new IdentityProvider<>(); + idp.setId("12345"); + idp.setName("some-name"); + idp.setOriginKey("some-origin"); + idp.setAliasZid("custom-zone"); + idp.setAliasId("id-of-mirrored-idp"); + idp.setActive(true); + idp.setIdentityZoneId(UAA); + final OIDCIdentityProviderDefinition config = new OIDCIdentityProviderDefinition(); + config.setIssuer("issuer"); + idp.setConfig(config); + } + + @Test + void testToString_ShouldContainAliasProperties() { + assertThat(idp).hasToString("IdentityProvider{id='12345', originKey='some-origin', name='some-name', type='oidc1.0', active=true, aliasId='id-of-mirrored-idp', aliasZid='custom-zone'}"); + } + +} \ No newline at end of file From d329dcb19797b5bb6bcb94b879cd46a68901486a Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 22 Dec 2023 11:10:21 +0100 Subject: [PATCH 43/91] Add unit test for IdentityProvider.equals and hashCode --- .../uaa/provider/IdentityProviderTest.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java b/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java index a7756b93d44..2c844385240 100644 --- a/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java +++ b/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java @@ -30,4 +30,32 @@ void testToString_ShouldContainAliasProperties() { assertThat(idp).hasToString("IdentityProvider{id='12345', originKey='some-origin', name='some-name', type='oidc1.0', active=true, aliasId='id-of-mirrored-idp', aliasZid='custom-zone'}"); } + @Test + void testEqualsAndHashCode() { + final IdentityProvider idp2 = new IdentityProvider<>(); + idp2.setId("12345"); + idp2.setName("some-name"); + idp2.setOriginKey("some-origin"); + idp2.setAliasZid("custom-zone"); + idp2.setAliasId("id-of-mirrored-idp"); + idp2.setActive(true); + idp2.setIdentityZoneId(UAA); + final OIDCIdentityProviderDefinition config = new OIDCIdentityProviderDefinition(); + config.setIssuer("issuer"); + idp2.setConfig(config); + + idp2.setCreated(idp.getCreated()); + idp2.setLastModified(idp.getLastModified()); + + assertThat(idp.equals(idp2)).isTrue(); + assertThat(idp).hasSameHashCodeAs(idp2); + + idp2.setAliasZid(null); + assertThat(idp.equals(idp2)).isFalse(); + + idp2.setAliasZid("custom-zone"); + idp2.setAliasId(null); + assertThat(idp.equals(idp2)).isFalse(); + } + } \ No newline at end of file From f16592822e4f82d45b4e3c837b4ddc13af02b271 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 22 Dec 2023 11:27:38 +0100 Subject: [PATCH 44/91] Improve unit test for IdentityProvider.equals and hashCode --- .../uaa/provider/IdentityProviderTest.java | 56 ++++++++++++------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java b/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java index 2c844385240..78681c8e681 100644 --- a/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java +++ b/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java @@ -3,16 +3,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; class IdentityProviderTest { - private static IdentityProvider idp; - - @BeforeAll - static void beforeAll() { - idp = new IdentityProvider<>(); + @Test + void testToString_ShouldContainAliasProperties() { + final IdentityProvider idp = new IdentityProvider<>(); idp.setId("12345"); idp.setName("some-name"); idp.setOriginKey("some-origin"); @@ -23,39 +20,56 @@ static void beforeAll() { final OIDCIdentityProviderDefinition config = new OIDCIdentityProviderDefinition(); config.setIssuer("issuer"); idp.setConfig(config); - } - @Test - void testToString_ShouldContainAliasProperties() { assertThat(idp).hasToString("IdentityProvider{id='12345', originKey='some-origin', name='some-name', type='oidc1.0', active=true, aliasId='id-of-mirrored-idp', aliasZid='custom-zone'}"); } @Test void testEqualsAndHashCode() { + final String customZoneId = "custom-zone"; + final String mirroredIdpId = "id-of-mirrored-idp"; + + final IdentityProvider idp1 = new IdentityProvider<>(); + idp1.setId("12345"); + idp1.setName("some-name"); + idp1.setOriginKey("some-origin"); + idp1.setAliasZid(customZoneId); + idp1.setAliasId(mirroredIdpId); + idp1.setActive(true); + idp1.setIdentityZoneId(UAA); + final OIDCIdentityProviderDefinition config1 = new OIDCIdentityProviderDefinition(); + config1.setIssuer("issuer"); + idp1.setConfig(config1); + final IdentityProvider idp2 = new IdentityProvider<>(); idp2.setId("12345"); idp2.setName("some-name"); idp2.setOriginKey("some-origin"); - idp2.setAliasZid("custom-zone"); - idp2.setAliasId("id-of-mirrored-idp"); + idp2.setAliasZid(customZoneId); + idp2.setAliasId(mirroredIdpId); idp2.setActive(true); idp2.setIdentityZoneId(UAA); - final OIDCIdentityProviderDefinition config = new OIDCIdentityProviderDefinition(); - config.setIssuer("issuer"); - idp2.setConfig(config); + final OIDCIdentityProviderDefinition config2 = new OIDCIdentityProviderDefinition(); + config2.setIssuer("issuer"); + idp2.setConfig(config2); - idp2.setCreated(idp.getCreated()); - idp2.setLastModified(idp.getLastModified()); + idp2.setCreated(idp1.getCreated()); + idp2.setLastModified(idp1.getLastModified()); - assertThat(idp.equals(idp2)).isTrue(); - assertThat(idp).hasSameHashCodeAs(idp2); + // initially, the tow IdPs should be equal + assertThat(idp1.equals(idp2)).isTrue(); + assertThat(idp1).hasSameHashCodeAs(idp2); + // remove aliasZid idp2.setAliasZid(null); - assertThat(idp.equals(idp2)).isFalse(); + assertThat(idp1.equals(idp2)).isFalse(); + assertThat(idp2.equals(idp1)).isFalse(); + idp2.setAliasZid(customZoneId); - idp2.setAliasZid("custom-zone"); + // remove aliasId idp2.setAliasId(null); - assertThat(idp.equals(idp2)).isFalse(); + assertThat(idp1.equals(idp2)).isFalse(); + assertThat(idp2.equals(idp1)).isFalse(); } } \ No newline at end of file From 2225d6b475cc708f1544c921d08185b9459858a4 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 5 Jan 2024 13:56:17 +0100 Subject: [PATCH 45/91] Fix access token cache in IdentityProviderEndpointsAliasMockMvcTests --- .../IdentityProviderEndpointsAliasMockMvcTests.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 6abbac25565..425142cd04d 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -61,7 +61,6 @@ */ @DefaultTestContext class IdentityProviderEndpointsAliasMockMvcTests { - private static final Map ACCESS_TOKEN_CACHE = new HashMap<>(); @Autowired private MockMvc mockMvc; @@ -72,6 +71,7 @@ class IdentityProviderEndpointsAliasMockMvcTests { @Autowired private WebApplicationContext webApplicationContext; + private static final Map accessTokenCache = new HashMap<>(); private IdentityZone customZone; private String adminToken; private String identityToken; @@ -640,7 +640,7 @@ private MvcResult createIdpAndReturnResult(final IdentityZone zone, final Identi } private String getAccessTokenForZone(final String zoneId) throws Exception { - final String cacheLookupResult = ACCESS_TOKEN_CACHE.get(zoneId); + final String cacheLookupResult = accessTokenCache.get(zoneId); if (cacheLookupResult != null) { return cacheLookupResult; } @@ -673,7 +673,7 @@ private String getAccessTokenForZone(final String zoneId) throws Exception { assertThat(resultingScopes).hasSameElementsAs(scopesForZone); // cache the access token - ACCESS_TOKEN_CACHE.put(zoneId, accessToken); + accessTokenCache.put(zoneId, accessToken); return accessToken; } From 2bf9edc88de7a143b9a204c4fb6f1d09fc242eca Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 5 Jan 2024 14:04:16 +0100 Subject: [PATCH 46/91] Make access token cache in IdentityProviderEndpointsAliasMockMvcTests non-static --- .../providers/IdentityProviderEndpointsAliasMockMvcTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 425142cd04d..ed7b1fbe7c4 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -71,7 +71,7 @@ class IdentityProviderEndpointsAliasMockMvcTests { @Autowired private WebApplicationContext webApplicationContext; - private static final Map accessTokenCache = new HashMap<>(); + private final Map accessTokenCache = new HashMap<>(); private IdentityZone customZone; private String adminToken; private String identityToken; From e2151397e4d237c38a59ac59bb7dfed3db345a5f Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 8 Jan 2024 17:07:33 +0100 Subject: [PATCH 47/91] Ignore dangling references to no longer existing mirrored IdPs during deletion --- .../provider/IdentityProviderEndpoints.java | 14 +++- ...ityProviderEndpointsAliasMockMvcTests.java | 73 ++++++++++++++----- 2 files changed, 67 insertions(+), 20 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 8f62e336848..b6d6f2f4dc9 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 @@ -179,9 +179,17 @@ public ResponseEntity deleteIdentityProvider(@PathVariable Str // delete mirrored IdP if alias fields are set if (hasText(existing.getAliasZid()) && hasText(existing.getAliasId())) { - IdentityProvider mirroredIdp = identityProviderProvisioning.retrieve(existing.getAliasId(), existing.getAliasZid()); - mirroredIdp.setSerializeConfigRaw(rawConfig); - publisher.publishEvent(new EntityDeletedEvent<>(mirroredIdp, authentication, identityZoneId)); + final IdentityProvider mirroredIdp = retrieveMirroredIdp(existing); + if (mirroredIdp != null) { + mirroredIdp.setSerializeConfigRaw(rawConfig); + publisher.publishEvent(new EntityDeletedEvent<>(mirroredIdp, authentication, identityZoneId)); + } else { + logger.warn( + "Mirrored IdP referenced in IdentityProvider[origin={}; zone={}}] not found, skipping deletion of mirrored IdP.", + existing.getOriginKey(), + existing.getIdentityZoneId() + ); + } } return new ResponseEntity<>(existing, OK); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index ed7b1fbe7c4..b1f196670af 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -285,9 +285,7 @@ private void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp(fi final IdentityProvider idp = createMirroredIdp(zone1, zone2); // delete the mirrored IdP directly in the DB -> after that, there is a dangling reference - final JdbcIdentityProviderProvisioning identityProviderProvisioning = webApplicationContext.getBean(JdbcIdentityProviderProvisioning.class); - final int rowsDeleted = identityProviderProvisioning.deleteByOrigin(idp.getOriginKey(), zone2.getId()); - assertThat(rowsDeleted).isEqualTo(1); + deleteIdpViaDb(idp.getOriginKey(), zone2.getId()); // update some other property on the original IdP idp.setName("some-new-name"); @@ -491,19 +489,26 @@ private void shouldRejectUpdate(final IdentityZone zone, final IdentityProvider< } } + private void deleteIdpViaDb(final String originKey, final String zoneId) { + final JdbcIdentityProviderProvisioning identityProviderProvisioning = webApplicationContext + .getBean(JdbcIdentityProviderProvisioning.class); + final int rowsDeleted = identityProviderProvisioning.deleteByOrigin(originKey, zoneId); + assertThat(rowsDeleted).isEqualTo(1); + } + @Nested class Delete { @Test - void shouldDeleteMirroredIdp_UaaToCustomZone() throws Exception { - shouldDeleteMirroredIdp(IdentityZone.getUaa(), customZone); + void shouldAlsoDeleteMirroredIdp_UaaToCustomZone() throws Exception { + shouldAlsoDeleteMirroredIdp(IdentityZone.getUaa(), customZone); } @Test - void shouldDeleteMirroredIdp_CustomToUaaZone() throws Exception { - shouldDeleteMirroredIdp(customZone, IdentityZone.getUaa()); + void shouldAlsoDeleteMirroredIdp_CustomToUaaZone() throws Exception { + shouldAlsoDeleteMirroredIdp(customZone, IdentityZone.getUaa()); } - private void shouldDeleteMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + private void shouldAlsoDeleteMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { final IdentityProvider idpInZone1 = createMirroredIdp(zone1, zone2); final String id = idpInZone1.getId(); assertThat(id).isNotBlank(); @@ -519,17 +524,48 @@ private void shouldDeleteMirroredIdp(final IdentityZone zone1, final IdentityZon assertThat(mirroredIdp.get().getAliasZid()).isNotBlank().isEqualTo(idpInZone1.getIdentityZoneId()); // delete IdP in zone 1 - final String accessTokenForZone1 = getAccessTokenForZone(zone1.getId()); + final MvcResult deleteResult = deleteIdpAndReturnResult(zone1, id); + assertThat(deleteResult.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); + + // check if IdP is no longer available in zone 2 + assertIdpDoesNotExist(zone2, aliasId); + } + + @Test + void shouldIgnoreDanglingReferenceToMirroredIdp_UaaToCustomZone() throws Exception { + shouldIgnoreDanglingReferenceToMirroredIdp(IdentityZone.getUaa(), customZone); + } + + @Test + void shouldIgnoreDanglingReferenceToMirroredIdp_CustomToUaaZone() throws Exception { + shouldIgnoreDanglingReferenceToMirroredIdp(customZone, IdentityZone.getUaa()); + } + + private void shouldIgnoreDanglingReferenceToMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + final IdentityProvider originalIdp = createMirroredIdp(zone1, zone2); + + // create a dangling reference by deleting the mirrored IdP directly in the DB + deleteIdpViaDb(originalIdp.getOriginKey(), zone2.getId()); + + // delete the original IdP -> dangling reference should be ignored + final MvcResult deleteResult = deleteIdpAndReturnResult(zone1, originalIdp.getId()); + assertThat(deleteResult.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); + + // original IdP should no longer exist + assertIdpDoesNotExist(zone1, originalIdp.getId()); + } + + private MvcResult deleteIdpAndReturnResult(final IdentityZone zone, final String id) throws Exception { + final String accessTokenForZone1 = getAccessTokenForZone(zone.getId()); final MockHttpServletRequestBuilder deleteRequestBuilder = delete("/identity-providers/" + id) .header("Authorization", "Bearer " + accessTokenForZone1) - .header(IdentityZoneSwitchingFilter.HEADER, zone1.getId()); - final MvcResult response = mockMvc.perform(deleteRequestBuilder).andReturn(); - - assertThat(response.getResponse().getStatus()).isEqualTo(200); + .header(IdentityZoneSwitchingFilter.HEADER, zone.getId()); + return mockMvc.perform(deleteRequestBuilder).andReturn(); + } - // check if IdP is no longer available in zone 2 - final Optional> mirroredIdpAfterDeletionOfOriginalIdp = readIdpFromZoneIfExists(zone2.getId(), aliasId); - assertThat(mirroredIdpAfterDeletionOfOriginalIdp).isNotPresent(); + private void assertIdpDoesNotExist(final IdentityZone zone, final String id) throws Exception { + final Optional> idp = readIdpFromZoneIfExists(zone.getId(), id); + assertThat(idp).isNotPresent(); } } @@ -620,7 +656,10 @@ private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final private IdentityProvider createMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { final IdentityProvider provider = buildIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); - return createIdp(zone1, provider); + final IdentityProvider createdOriginalIdp = createIdp(zone1, provider); + assertThat(createdOriginalIdp.getAliasId()).isNotBlank(); + assertThat(createdOriginalIdp.getAliasZid()).isNotBlank(); + return createdOriginalIdp; } private IdentityProvider createIdp(final IdentityZone zone, final IdentityProvider idp) throws Exception { From c4c97c6958e10114151e681513d32817cbe94159 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 10 Jan 2024 17:17:58 +0100 Subject: [PATCH 48/91] Add identity zone ID field to IdentityProvider#toString --- .../identity/uaa/provider/IdentityProvider.java | 8 ++++++++ .../identity/uaa/provider/IdentityProviderTest.java | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProvider.java b/model/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProvider.java index cde3ab482da..667c333c980 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProvider.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProvider.java @@ -305,6 +305,14 @@ public boolean equals(Object obj) { public String toString() { final StringBuffer sb = new StringBuffer("IdentityProvider{"); sb.append("id='").append(id).append('\''); + + sb.append(", identityZoneId="); + if (identityZoneId != null) { + sb.append('\'').append(identityZoneId).append('\''); + } else { + sb.append("null"); + } + sb.append(", originKey='").append(originKey).append('\''); sb.append(", name='").append(name).append('\''); sb.append(", type='").append(type).append('\''); diff --git a/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java b/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java index 78681c8e681..a59fedd2c62 100644 --- a/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java +++ b/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java @@ -21,7 +21,7 @@ void testToString_ShouldContainAliasProperties() { config.setIssuer("issuer"); idp.setConfig(config); - assertThat(idp).hasToString("IdentityProvider{id='12345', originKey='some-origin', name='some-name', type='oidc1.0', active=true, aliasId='id-of-mirrored-idp', aliasZid='custom-zone'}"); + assertThat(idp).hasToString("IdentityProvider{id='12345', identityZoneId='uaa', originKey='some-origin', name='some-name', type='oidc1.0', active=true, aliasId='id-of-mirrored-idp', aliasZid='custom-zone'}"); } @Test From 7fda6f064d43ef77b742208ba85f5927a35a0881 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 10 Jan 2024 17:36:53 +0100 Subject: [PATCH 49/91] Use AlphanumericRandomValueStringGenerator in IdentityProviderEndpointsAliasMockMvcTests --- .../IdentityProviderEndpointsAliasMockMvcTests.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index b1f196670af..ecbea01e263 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -23,7 +23,6 @@ import java.util.UUID; import java.util.stream.Stream; -import org.apache.commons.lang.RandomStringUtils; import org.cloudfoundry.identity.uaa.DefaultTestContext; import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; @@ -37,6 +36,7 @@ import org.cloudfoundry.identity.uaa.provider.saml.BootstrapSamlIdentityProviderDataTests; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.test.TestClient; +import org.cloudfoundry.identity.uaa.util.AlphanumericRandomValueStringGenerator; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.UaaTokenUtils; import org.cloudfoundry.identity.uaa.zone.IdentityZone; @@ -61,6 +61,7 @@ */ @DefaultTestContext class IdentityProviderEndpointsAliasMockMvcTests { + private static final AlphanumericRandomValueStringGenerator RANDOM_STRING_GENERATOR = new AlphanumericRandomValueStringGenerator(8); @Autowired private MockMvc mockMvc; @@ -172,7 +173,7 @@ void shouldReject_IdpWithOriginAlreadyExistsInAliasZone_UaaToCustomZone() throws } private void shouldReject_IdpWithOriginAlreadyExistsInAliasZone(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final String originKey = RandomStringUtils.randomAlphabetic(10); + final String originKey = RANDOM_STRING_GENERATOR.generate(); // create IdP with origin key in custom zone final IdentityProvider createdIdp1 = createIdp( @@ -361,7 +362,7 @@ void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone_CustomToUaaZone() th } private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final String originKey = RandomStringUtils.randomAlphabetic(10); + final String originKey = RANDOM_STRING_GENERATOR.generate(); // create IdP with origin key in zone 2 final IdentityProvider existingIdpInZone2 = buildIdpWithAliasProperties( @@ -586,7 +587,7 @@ private void shouldAccept_MirroredIdpShouldAlsoBeUpdated(final IdentityZone zone final IdentityProvider idp = new IdentityProvider<>(); idp.setType(OriginKeys.UAA); idp.setName("some-name"); - idp.setOriginKey(RandomStringUtils.randomAlphabetic(8)); + idp.setOriginKey(RANDOM_STRING_GENERATOR.generate()); final PasswordPolicy passwordPolicy = new PasswordPolicy(); passwordPolicy.setExpirePasswordInMonths(1); passwordPolicy.setMaxLength(100); @@ -756,7 +757,7 @@ private static List getScopesForZone(final String zoneId, final String.. } private static IdentityProvider buildIdpWithAliasProperties(final String idzId, final String aliasId, final String aliasZid) { - final String originKey = RandomStringUtils.randomAlphabetic(8); + final String originKey = RANDOM_STRING_GENERATOR.generate(); return buildIdpWithAliasProperties(idzId, aliasId, aliasZid, originKey); } From 862405896a1bcb8da76042569c71c72705c1ea35 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 10 Jan 2024 18:39:47 +0100 Subject: [PATCH 50/91] Restrict mirroring to a fixed set of IdP types --- .../provider/IdentityProviderEndpoints.java | 105 ++++---- ...ityProviderEndpointsAliasMockMvcTests.java | 227 +++++++++--------- 2 files changed, 166 insertions(+), 166 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 b6d6f2f4dc9..a0602fbbe39 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 @@ -13,21 +13,46 @@ */ package org.cloudfoundry.identity.uaa.provider; -import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; -import org.cloudfoundry.identity.uaa.zone.ZoneDoesNotExistsException; -import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LDAP; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OIDC10; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.SAML; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; +import static org.cloudfoundry.identity.uaa.util.UaaStringUtils.getCleanedUserControlString; +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; +import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; +import static org.springframework.util.StringUtils.hasText; +import static org.springframework.web.bind.annotation.RequestMethod.DELETE; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.PATCH; +import static org.springframework.web.bind.annotation.RequestMethod.POST; +import static org.springframework.web.bind.annotation.RequestMethod.PUT; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Date; +import java.util.List; +import java.util.Set; + +import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent; import org.cloudfoundry.identity.uaa.authentication.manager.DynamicLdapAuthenticationManager; import org.cloudfoundry.identity.uaa.authentication.manager.LdapLoginAuthenticationManager; -import org.cloudfoundry.identity.uaa.constants.OriginKeys; -import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent; import org.cloudfoundry.identity.uaa.provider.saml.SamlIdentityProviderConfigurator; import org.cloudfoundry.identity.uaa.scim.ScimGroupExternalMembershipManager; import org.cloudfoundry.identity.uaa.scim.ScimGroupProvisioning; import org.cloudfoundry.identity.uaa.util.JsonUtils; import org.cloudfoundry.identity.uaa.util.ObjectUtils; +import org.cloudfoundry.identity.uaa.zone.IdentityZoneProvisioning; +import org.cloudfoundry.identity.uaa.zone.ZoneDoesNotExistsException; +import org.cloudfoundry.identity.uaa.zone.beans.IdentityZoneManager; import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; @@ -51,36 +76,17 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.Date; -import java.util.List; - -import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LDAP; -import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20; -import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OIDC10; -import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; -import static org.cloudfoundry.identity.uaa.util.UaaStringUtils.getCleanedUserControlString; -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; -import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; -import static org.springframework.util.StringUtils.hasText; -import static org.springframework.web.bind.annotation.RequestMethod.DELETE; -import static org.springframework.web.bind.annotation.RequestMethod.GET; -import static org.springframework.web.bind.annotation.RequestMethod.PATCH; -import static org.springframework.web.bind.annotation.RequestMethod.POST; -import static org.springframework.web.bind.annotation.RequestMethod.PUT; - @RequestMapping("/identity-providers") @RestController public class IdentityProviderEndpoints implements ApplicationEventPublisherAware { protected static Logger logger = LoggerFactory.getLogger(IdentityProviderEndpoints.class); + /** + * The IdP types for which mirroring via 'alias_id' and 'alias_zid' is supported. + */ + private static final Set IDP_TYPES_MIRRORING_SUPPORTED = Set.of(SAML, OAUTH20, OIDC10); + private final IdentityProviderProvisioning identityProviderProvisioning; private final ScimGroupExternalMembershipManager scimGroupExternalMembershipManager; private final ScimGroupProvisioning scimGroupProvisioning; @@ -129,7 +135,7 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden logger.debug("IdentityProvider[origin="+body.getOriginKey()+"; zone="+body.getIdentityZoneId()+"] - Configuration validation error.", e); return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } - if (OriginKeys.SAML.equals(body.getType())) { + if (SAML.equals(body.getType())) { SamlIdentityProviderDefinition definition = ObjectUtils.castInstance(body.getConfig(), SamlIdentityProviderDefinition.class); definition.setZoneId(zoneId); definition.setIdpEntityAlias(body.getOriginKey()); @@ -219,7 +225,7 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } - if (OriginKeys.SAML.equals(body.getType())) { + if (SAML.equals(body.getType())) { body.setOriginKey(existing.getOriginKey()); //we do not allow origin to change for a SAML provider, since that can cause clashes SamlIdentityProviderDefinition definition = ObjectUtils.castInstance(body.getConfig(), SamlIdentityProviderDefinition.class); definition.setZoneId(zoneId); @@ -263,30 +269,12 @@ public ResponseEntity updateIdentityProviderStatus(@Path logger.debug("IDP does not have an existing PasswordPolicy. Operation not supported"); return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } + uaaIdentityProviderDefinition.getPasswordPolicy().setPasswordNewerThan(new Date(System.currentTimeMillis())); + identityProviderProvisioning.update(existing, zoneId); + logger.info("PasswordChangeRequired property set for Identity Provider: " + existing.getId()); - final Date passwordNewerThanTimestamp = new Date(System.currentTimeMillis()); - uaaIdentityProviderDefinition.getPasswordPolicy().setPasswordNewerThan(passwordNewerThanTimestamp); - - // update the property in the mirrored IdP if present - final IdentityProvider mirroredIdp; - if (hasText(existing.getAliasZid()) && hasText(existing.getAliasId())) { - mirroredIdp = identityProviderProvisioning.retrieve(existing.getAliasId(), existing.getAliasZid()); - final UaaIdentityProviderDefinition definitionMirroredIdp = ObjectUtils.castInstance( - mirroredIdp.getConfig(), - UaaIdentityProviderDefinition.class - ); - definitionMirroredIdp.getPasswordPolicy().setPasswordNewerThan(passwordNewerThanTimestamp); - } else { - mirroredIdp = null; - } - - // update both IdPs in a transaction - transactionTemplate.executeWithoutResult(txStatus -> { - identityProviderProvisioning.update(existing, zoneId); - if (mirroredIdp != null) { - identityProviderProvisioning.update(mirroredIdp, mirroredIdp.getIdentityZoneId()); - } - }); + /* since this operation is only allowed for IdPs of type "UAA" and mirroring is not supported for "UAA" IdPs, + * we do not need to propagate the changes to a mirrored IdP here. */ logger.info("PasswordChangeRequired property set for Identity Provider: {}", existing.getId()); return new ResponseEntity<>(body, OK); @@ -377,6 +365,11 @@ private boolean aliasPropertiesAreValid( return true; } + // check if mirroring is supported for this IdP type + if (!IDP_TYPES_MIRRORING_SUPPORTED.contains(requestBody.getType())) { + return false; + } + // the referenced zone must exist try { identityZoneProvisioning.retrieve(requestBody.getAliasZid()); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index ecbea01e263..d3fa16d52ee 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -5,10 +5,11 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import static org.assertj.core.api.Assertions.assertThat; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.SAML; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; 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.patch; 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; @@ -24,11 +25,10 @@ import java.util.stream.Stream; import org.cloudfoundry.identity.uaa.DefaultTestContext; -import org.cloudfoundry.identity.uaa.constants.OriginKeys; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; import org.cloudfoundry.identity.uaa.oauth.token.TokenConstants; +import org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.IdentityProvider; -import org.cloudfoundry.identity.uaa.provider.IdentityProviderStatus; import org.cloudfoundry.identity.uaa.provider.JdbcIdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.provider.PasswordPolicy; import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; @@ -104,7 +104,7 @@ void shouldAccept_MirrorIdp_CustomToUaaZone() throws Exception { private void shouldAccept_MirrorIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { // build IdP in zone1 with aliasZid set to zone2 - final IdentityProvider provider = buildIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); + final IdentityProvider provider = buildSamlIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); // create IdP in zone1 final IdentityProvider originalIdp = createIdp(zone1, provider); @@ -134,27 +134,42 @@ void shouldReject_IdzAndAliasZidAreEqual_CustomZone() throws Exception { } private void shouldReject_IdzAndAliasZidAreEqual(final IdentityZone zone) throws Exception { - final IdentityProvider idp = buildIdpWithAliasProperties(zone.getId(), null, zone.getId()); + final IdentityProvider idp = buildSamlIdpWithAliasProperties(zone.getId(), null, zone.getId()); shouldRejectCreation(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } + @Test + void shouldReject_MirroringNotSupportedForIdpType_UaaToCustomZone() throws Exception { + shouldReject_MirroringNotSupportedForIdpType(IdentityZone.getUaa(), customZone); + } + + @Test + void shouldReject_MirroringNotSupportedForIdpType_CustomToUaaZone() throws Exception { + shouldReject_MirroringNotSupportedForIdpType(customZone, IdentityZone.getUaa()); + } + + private void shouldReject_MirroringNotSupportedForIdpType(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + final IdentityProvider uaaIdp = buildUaaIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); + shouldRejectCreation(zone1, uaaIdp, HttpStatus.UNPROCESSABLE_ENTITY); + } + @Test void shouldReject_NeitherIdzNorAliasZidIsUaa() throws Exception { final IdentityZone otherCustomZone = MockMvcUtils.createZoneUsingWebRequest(mockMvc, identityToken); - final IdentityProvider idp = buildIdpWithAliasProperties(customZone.getId(), null, otherCustomZone.getId()); + final IdentityProvider idp = buildSamlIdpWithAliasProperties(customZone.getId(), null, otherCustomZone.getId()); shouldRejectCreation(customZone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } @Test void shouldReject_AliasIdIsSet() throws Exception { final String aliasId = UUID.randomUUID().toString(); - final IdentityProvider idp = buildIdpWithAliasProperties(customZone.getId(), aliasId, IdentityZone.getUaaZoneId()); + final IdentityProvider idp = buildSamlIdpWithAliasProperties(customZone.getId(), aliasId, IdentityZone.getUaaZoneId()); shouldRejectCreation(customZone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } @Test void shouldReject_IdzReferencedInAliasZidDoesNotExist() throws Exception { - final IdentityProvider provider = buildIdpWithAliasProperties( + final IdentityProvider provider = buildSamlIdpWithAliasProperties( IdentityZone.getUaaZoneId(), null, UUID.randomUUID().toString() // does not exist @@ -173,19 +188,17 @@ void shouldReject_IdpWithOriginAlreadyExistsInAliasZone_UaaToCustomZone() throws } private void shouldReject_IdpWithOriginAlreadyExistsInAliasZone(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final String originKey = RANDOM_STRING_GENERATOR.generate(); - // create IdP with origin key in custom zone final IdentityProvider createdIdp1 = createIdp( zone1, - buildIdpWithAliasProperties(zone1.getId(), null, null, originKey) + buildSamlIdpWithAliasProperties(zone1.getId(), null, null) ); assertThat(createdIdp1).isNotNull(); // then, create an IdP in the "uaa" zone with the same origin key that should be mirrored to the custom zone shouldRejectCreation( zone2, - buildIdpWithAliasProperties(zone2.getId(), null, zone1.getId(), originKey), + buildIdpWithAliasProperties(zone2.getId(), null, zone1.getId(), createdIdp1.getOriginKey(), SAML), HttpStatus.CONFLICT ); } @@ -219,7 +232,7 @@ private void shouldAccept_ShouldCreateMirroredIdp(final IdentityZone zone1, fina // create regular idp without alias properties in UAA zone final IdentityProvider existingIdpWithoutAlias = createIdp( zone1, - buildIdpWithAliasProperties(zone1.getId(), null, null) + buildSamlIdpWithAliasProperties(zone1.getId(), null, null) ); assertThat(existingIdpWithoutAlias).isNotNull(); assertThat(existingIdpWithoutAlias.getId()).isNotBlank(); @@ -344,13 +357,33 @@ void shouldReject_OnlyAliasIdSet_CustomZone() throws Exception { } private void shouldReject_OnlyAliasIdSet(final IdentityZone zone) throws Exception { - final IdentityProvider idp = buildIdpWithAliasProperties(zone.getId(), null, null); + final IdentityProvider idp = buildSamlIdpWithAliasProperties(zone.getId(), null, null); final IdentityProvider createdProvider = createIdp(zone, idp); assertThat(createdProvider.getAliasZid()).isBlank(); createdProvider.setAliasId(UUID.randomUUID().toString()); shouldRejectUpdate(zone, createdProvider, HttpStatus.UNPROCESSABLE_ENTITY); } + @Test + void shouldReject_MirroringNotSupportedForIdpType_UaaToCustomZone() throws Exception { + shouldReject_MirroringNotSupportedForIdpType(IdentityZone.getUaa(), customZone); + } + + @Test + void shouldReject_MirroringNotSupportedForIdpType_CustomZone() throws Exception { + shouldReject_MirroringNotSupportedForIdpType(customZone, IdentityZone.getUaa()); + } + + private void shouldReject_MirroringNotSupportedForIdpType(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + final IdentityProvider uaaIdp = buildUaaIdpWithAliasProperties(zone1.getId(), null, null); + final IdentityProvider createdProvider = createIdp(zone1, uaaIdp); + assertThat(createdProvider.getAliasZid()).isBlank(); + + // try to mirror the IdP -> should fail because of the IdP's type + createdProvider.setAliasZid(zone2.getId()); + shouldRejectUpdate(zone1, createdProvider, HttpStatus.UNPROCESSABLE_ENTITY); + } + @Test void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone_UaaToCustomZone() throws Exception { shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(IdentityZone.getUaa(), customZone); @@ -362,15 +395,8 @@ void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone_CustomToUaaZone() th } private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final String originKey = RANDOM_STRING_GENERATOR.generate(); - // create IdP with origin key in zone 2 - final IdentityProvider existingIdpInZone2 = buildIdpWithAliasProperties( - zone2.getId(), - null, - null, - originKey - ); + final IdentityProvider existingIdpInZone2 = buildSamlIdpWithAliasProperties(zone2.getId(), null, null); createIdp(zone2, existingIdpInZone2); // create IdP with same origin key in zone 1 @@ -378,7 +404,8 @@ private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final Identi zone1.getId(), null, null, - originKey // same origin key + existingIdpInZone2.getOriginKey(), // same origin key + SAML ); final IdentityProvider providerInZone1 = createIdp(zone1, idp); @@ -391,7 +418,7 @@ private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final Identi void shouldReject_IdpInCustomZoneMirroredToOtherCustomZone() throws Exception { final IdentityProvider idpInCustomZone = createIdp( customZone, - buildIdpWithAliasProperties(customZone.getId(), null, null) + buildSamlIdpWithAliasProperties(customZone.getId(), null, null) ); // try to mirror it to another custom zone @@ -412,7 +439,7 @@ void shouldReject_AliasZidSetToSameZone_CustomZone() throws Exception { private void shouldReject_AliasZidSetToSameZone(final IdentityZone zone) throws Exception { final IdentityProvider idp = createIdp( zone, - buildIdpWithAliasProperties(zone.getId(), null, null) + buildSamlIdpWithAliasProperties(zone.getId(), null, null) ); idp.setAliasZid(zone.getId()); shouldRejectUpdate(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); @@ -570,70 +597,6 @@ private void assertIdpDoesNotExist(final IdentityZone zone, final String id) thr } } - @Nested - class UpdateStatus { - @Test - void shouldAccept_MirroredIdpShouldAlsoBeUpdated_UaaToCustomZone() throws Exception { - shouldAccept_MirroredIdpShouldAlsoBeUpdated(IdentityZone.getUaa(), customZone); - } - - @Test - void shouldAccept_MirroredIdpShouldAlsoBeUpdated_CustomToUaaZone() throws Exception { - shouldAccept_MirroredIdpShouldAlsoBeUpdated(customZone, IdentityZone.getUaa()); - } - - private void shouldAccept_MirroredIdpShouldAlsoBeUpdated(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - // create an IdP of type UAA - final IdentityProvider idp = new IdentityProvider<>(); - idp.setType(OriginKeys.UAA); - idp.setName("some-name"); - idp.setOriginKey(RANDOM_STRING_GENERATOR.generate()); - final PasswordPolicy passwordPolicy = new PasswordPolicy(); - passwordPolicy.setExpirePasswordInMonths(1); - passwordPolicy.setMaxLength(100); - passwordPolicy.setMinLength(10); - passwordPolicy.setRequireDigit(1); - passwordPolicy.setRequireUpperCaseCharacter(1); - passwordPolicy.setRequireLowerCaseCharacter(1); - passwordPolicy.setRequireSpecialCharacter(1); - passwordPolicy.setPasswordNewerThan(new Date(System.currentTimeMillis())); - idp.setConfig(new UaaIdentityProviderDefinition(passwordPolicy, null)); - idp.setAliasZid(zone2.getId()); - final String accessTokenForZone1 = getAccessTokenForZone(zone1.getId()); - final IdentityProvider createdIdp = createIdp(zone1, idp); - - final Date timestampBeforeUpdate = getPasswordNewerThanTimestamp(createdIdp); - assertThat(timestampBeforeUpdate).isNotNull(); - - final IdentityProviderStatus identityProviderStatus = new IdentityProviderStatus(); - identityProviderStatus.setRequirePasswordChange(true); - final MockHttpServletRequestBuilder updateRequestBuilder = patch("/identity-providers/" + createdIdp.getId() + "/status") - .header("Authorization", "Bearer " + accessTokenForZone1) - .header(IdentityZoneSwitchingFilter.HEADER, zone1.getId()) - .contentType(APPLICATION_JSON) - .content(JsonUtils.writeValueAsString(identityProviderStatus)); - mockMvc.perform(updateRequestBuilder).andExpect(status().isOk()).andReturn(); - - // check if timestamp is updated in zone 1 - final String id1 = createdIdp.getId(); - final Optional> idpInZone1 = readIdpFromZoneIfExists(zone1.getId(), id1); - assertThat(idpInZone1).isPresent(); - final Date timestampAfterUpdate = getPasswordNewerThanTimestamp(idpInZone1.get()); - assertThat(timestampAfterUpdate).isAfter(timestampBeforeUpdate); - - // check if timestamp is updated in zone 2 - final String id = createdIdp.getAliasId(); - final Optional> idpInZone2 = readIdpFromZoneIfExists(zone2.getId(), id); - assertThat(idpInZone2).isPresent(); - final Date timestampAfterUpdateMirroredIdp = getPasswordNewerThanTimestamp(idpInZone2.get()); - assertThat(timestampAfterUpdateMirroredIdp).isEqualTo(timestampAfterUpdate); - } - - private Date getPasswordNewerThanTimestamp(final IdentityProvider idp) { - return ((UaaIdentityProviderDefinition) idp.getConfig()).getPasswordPolicy().getPasswordNewerThan(); - } - } - private void assertIdpReferencesOtherIdp(final IdentityProvider idp, final IdentityProvider referencedIdp) { assertThat(idp).isNotNull(); assertThat(referencedIdp).isNotNull(); @@ -656,7 +619,7 @@ private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final } private IdentityProvider createMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final IdentityProvider provider = buildIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); + final IdentityProvider provider = buildSamlIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); final IdentityProvider createdOriginalIdp = createIdp(zone1, provider); assertThat(createdOriginalIdp.getAliasId()).isNotBlank(); assertThat(createdOriginalIdp.getAliasZid()).isNotBlank(); @@ -756,32 +719,76 @@ private static List getScopesForZone(final String zoneId, final String.. return Stream.of(scopes).map(scope -> String.format("zones.%s.%s", zoneId, scope)).collect(toList()); } - private static IdentityProvider buildIdpWithAliasProperties(final String idzId, final String aliasId, final String aliasZid) { + private static IdentityProvider buildSamlIdpWithAliasProperties( + final String idzId, + final String aliasId, + final String aliasZid + ) { final String originKey = RANDOM_STRING_GENERATOR.generate(); - return buildIdpWithAliasProperties(idzId, aliasId, aliasZid, originKey); + return buildIdpWithAliasProperties(idzId, aliasId, aliasZid, originKey, SAML); } - private static IdentityProvider buildIdpWithAliasProperties(final String idzId, final String aliasId, final String aliasZid, final String originKey) { - final String metadata = String.format( - BootstrapSamlIdentityProviderDataTests.xmlWithoutID, - "http://localhost:9999/metadata/" + originKey - ); - final SamlIdentityProviderDefinition samlDefinition = new SamlIdentityProviderDefinition() - .setMetaDataLocation(metadata) - .setLinkText("Test SAML Provider"); - samlDefinition.setEmailDomain(Arrays.asList("test.com", "test2.com")); - samlDefinition.setExternalGroupsWhitelist(singletonList("value")); - samlDefinition.setAttributeMappings(singletonMap("given_name", "first_name")); - - final IdentityProvider provider = new IdentityProvider<>(); - provider.setActive(true); - provider.setName(originKey); + private IdentityProvider buildUaaIdpWithAliasProperties( + final String idzId, + final String aliasId, + final String aliasZid + ) { + final String originKey = RANDOM_STRING_GENERATOR.generate(); + return buildIdpWithAliasProperties(idzId, aliasId, aliasZid, originKey, UAA); + } + + private static IdentityProvider buildIdpWithAliasProperties( + final String idzId, + final String aliasId, + final String aliasZid, + final String originKey, + final String type + ) { + final AbstractIdentityProviderDefinition definition = buildIdpDefinition(originKey, type); + + final IdentityProvider provider = new IdentityProvider<>(); provider.setIdentityZoneId(idzId); - provider.setType(OriginKeys.SAML); - provider.setOriginKey(originKey); - provider.setConfig(samlDefinition); provider.setAliasId(aliasId); provider.setAliasZid(aliasZid); + provider.setName(originKey); + provider.setOriginKey(originKey); + provider.setType(type); + provider.setConfig(definition); + provider.setActive(true); return provider; } + + private static AbstractIdentityProviderDefinition buildIdpDefinition(final String originKey, final String type) { + switch (type) { + case SAML: + final String metadata = String.format( + BootstrapSamlIdentityProviderDataTests.xmlWithoutID, + "http://localhost:9999/metadata/" + originKey + ); + final SamlIdentityProviderDefinition samlDefinition = new SamlIdentityProviderDefinition() + .setMetaDataLocation(metadata) + .setLinkText("Test SAML Provider"); + samlDefinition.setEmailDomain(Arrays.asList("test.com", "test2.com")); + samlDefinition.setExternalGroupsWhitelist(singletonList("value")); + samlDefinition.setAttributeMappings(singletonMap("given_name", "first_name")); + + return samlDefinition; + case UAA: + final PasswordPolicy passwordPolicy = new PasswordPolicy(); + passwordPolicy.setExpirePasswordInMonths(1); + passwordPolicy.setMaxLength(100); + passwordPolicy.setMinLength(10); + passwordPolicy.setRequireDigit(1); + passwordPolicy.setRequireUpperCaseCharacter(1); + passwordPolicy.setRequireLowerCaseCharacter(1); + passwordPolicy.setRequireSpecialCharacter(1); + passwordPolicy.setPasswordNewerThan(new Date(System.currentTimeMillis())); + final UaaIdentityProviderDefinition uaaDefinition = new UaaIdentityProviderDefinition(); + uaaDefinition.setPasswordPolicy(new PasswordPolicy()); + + return uaaDefinition; + default: + throw new IllegalArgumentException("IdP type not supported."); + } + } } From cc5ed2deeb3e06b47dc6b581f65192a72c633211 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 10 Jan 2024 18:47:13 +0100 Subject: [PATCH 51/91] Remove obsolete unit test for update status of mirrored IdP --- .../IdentityProviderEndpointsTest.java | 58 ------------------- 1 file changed, 58 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index 97f25721aeb..8edd3944fc2 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -29,7 +29,6 @@ import java.net.URL; import java.util.ArrayList; import java.util.Arrays; -import java.util.Date; import java.util.List; import java.util.Objects; import java.util.UUID; @@ -59,8 +58,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.transaction.PlatformTransactionManager; -import com.sun.mail.imap.protocol.ID; - @ExtendWith(PollutionPreventionExtension.class) @ExtendWith(MockitoExtension.class) class IdentityProviderEndpointsTest { @@ -441,61 +438,6 @@ void testUpdateIdentityProvider_AlreadyMirrored_ValidChange() throws MetadataPro Assertions.assertThat(secondIdp.getName()).isEqualTo(newName); } - @Test - void testUpdateStatus_ShouldAlsoUpdateMirroredIdpIfPresent() { - when(mockIdentityZoneManager.getCurrentIdentityZoneId()).thenReturn(UAA); - - final IdentityProvider idp = new IdentityProvider<>(); - final String idpId = UUID.randomUUID().toString(); - idp.setId(idpId); - idp.setIdentityZoneId(UAA); - final String mirroredIdpId = UUID.randomUUID().toString(); - idp.setAliasId(mirroredIdpId); - final String customZoneId = UUID.randomUUID().toString(); - idp.setAliasZid(customZoneId); - final UaaIdentityProviderDefinition config = new UaaIdentityProviderDefinition(); - final PasswordPolicy passwordPolicy = new PasswordPolicy(); - config.setPasswordPolicy(passwordPolicy); - idp.setConfig(config); - when(mockIdentityProviderProvisioning.retrieve(idpId, UAA)).thenReturn(idp); - - final IdentityProvider mirroredIdp = new IdentityProvider<>(); - mirroredIdp.setId(mirroredIdpId); - mirroredIdp.setIdentityZoneId(customZoneId); - mirroredIdp.setAliasId(idpId); - mirroredIdp.setAliasZid(UAA); - mirroredIdp.setConfig(config); - when(mockIdentityProviderProvisioning.retrieve(mirroredIdpId, customZoneId)).thenReturn(mirroredIdp); - - when(mockIdentityProviderProvisioning.update(any(), anyString())).thenReturn(null); - - final Date timestampBeforeUpdate = new Date(System.currentTimeMillis()); - - final IdentityProviderStatus requestBody = new IdentityProviderStatus(); - requestBody.setRequirePasswordChange(true); - final ResponseEntity response = identityProviderEndpoints.updateIdentityProviderStatus(idpId, requestBody); - - Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - Assertions.assertThat(response.getBody()).isNotNull().isEqualTo(requestBody); - - final ArgumentCaptor idpArgumentCaptor = ArgumentCaptor.forClass(IdentityProvider.class); - verify(mockIdentityProviderProvisioning, times(2)).update(idpArgumentCaptor.capture(), anyString()); - - // expecting original IdP with a new timestamp - final IdentityProvider firstIdp = idpArgumentCaptor.getAllValues().get(0); - Assertions.assertThat(firstIdp).isNotNull(); - Assertions.assertThat(firstIdp.getId()).isEqualTo(idpId); - final Date timestampAfterUpdateFirstIdp = ((UaaIdentityProviderDefinition) firstIdp.getConfig()).getPasswordPolicy().getPasswordNewerThan(); - Assertions.assertThat(timestampAfterUpdateFirstIdp).isNotNull().isAfter(timestampBeforeUpdate); - - // expecting mirrored IdP with same timestamp - final IdentityProvider secondIdp = idpArgumentCaptor.getAllValues().get(1); - Assertions.assertThat(secondIdp).isNotNull(); - Assertions.assertThat(secondIdp.getId()).isEqualTo(mirroredIdpId); - final Date timestampAfterUpdateSecondIdp = ((UaaIdentityProviderDefinition) secondIdp.getConfig()).getPasswordPolicy().getPasswordNewerThan(); - Assertions.assertThat(timestampAfterUpdateFirstIdp).isNotNull().isEqualTo(timestampAfterUpdateSecondIdp); - } - @Test void create_ldap_provider_removes_password() throws Exception { String zoneId = IdentityZone.getUaaZoneId(); From 10614d4e41b431d798cf8f61f1237992aa834534 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 10 Jan 2024 19:01:52 +0100 Subject: [PATCH 52/91] Replace transaction manager usage with Transactional annotation --- .../provider/IdentityProviderEndpoints.java | 40 ++++++++----------- .../IdentityProviderEndpointsTest.java | 4 -- 2 files changed, 17 insertions(+), 27 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 a0602fbbe39..8766f256fd9 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 @@ -66,9 +66,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.TransactionTemplate; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -95,7 +93,6 @@ public class IdentityProviderEndpoints implements ApplicationEventPublisherAware private final IdentityProviderConfigValidator configValidator; private final IdentityZoneManager identityZoneManager; private final IdentityZoneProvisioning identityZoneProvisioning; - private final TransactionTemplate transactionTemplate; private ApplicationEventPublisher publisher = null; @@ -111,8 +108,7 @@ public IdentityProviderEndpoints( final @Qualifier("metaDataProviders") SamlIdentityProviderConfigurator samlConfigurator, final @Qualifier("identityProviderConfigValidator") IdentityProviderConfigValidator configValidator, final IdentityZoneManager identityZoneManager, - final @Qualifier("identityZoneProvisioning") IdentityZoneProvisioning identityZoneProvisioning, - final @Qualifier("transactionManager") PlatformTransactionManager transactionManager + final @Qualifier("identityZoneProvisioning") IdentityZoneProvisioning identityZoneProvisioning ) { this.identityProviderProvisioning = identityProviderProvisioning; this.scimGroupExternalMembershipManager = scimGroupExternalMembershipManager; @@ -121,10 +117,10 @@ public IdentityProviderEndpoints( this.configValidator = configValidator; this.identityZoneManager = identityZoneManager; this.identityZoneProvisioning = identityZoneProvisioning; - this.transactionTemplate = new TransactionTemplate(transactionManager); } @RequestMapping(method = POST) + @Transactional public ResponseEntity createIdentityProvider(@RequestBody IdentityProvider body, @RequestParam(required = false, defaultValue = "false") boolean rawConfig) throws MetadataProviderException{ body.setSerializeConfigRaw(rawConfig); String zoneId = identityZoneManager.getCurrentIdentityZoneId(); @@ -148,15 +144,10 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden } // persist IdP and mirror if necessary - final IdentityProvider createdIdp; + final IdentityProvider createdIdpAfterMirrorHandling; try { - createdIdp = transactionTemplate.execute(txStatus -> { - final IdentityProvider createdOriginalIdp = identityProviderProvisioning.create(body, zoneId); - createdOriginalIdp.setSerializeConfigRaw(rawConfig); - redactSensitiveData(createdOriginalIdp); - - return ensureConsistencyOfMirroredIdp(createdOriginalIdp); - }); + final IdentityProvider createdIdp = identityProviderProvisioning.create(body, zoneId); + createdIdpAfterMirrorHandling = ensureConsistencyOfMirroredIdp(createdIdp); } catch (final IdpAlreadyExistsException e) { return new ResponseEntity<>(body, CONFLICT); } catch (final Exception e) { @@ -164,7 +155,9 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden return new ResponseEntity<>(body, INTERNAL_SERVER_ERROR); } - return new ResponseEntity<>(createdIdp, CREATED); + createdIdpAfterMirrorHandling.setSerializeConfigRaw(rawConfig); + redactSensitiveData(createdIdpAfterMirrorHandling); + return new ResponseEntity<>(createdIdpAfterMirrorHandling, CREATED); } @RequestMapping(value = "{id}", method = DELETE) @@ -202,6 +195,7 @@ public ResponseEntity deleteIdentityProvider(@PathVariable Str } @RequestMapping(value = "{id}", method = PUT) + @Transactional public ResponseEntity updateIdentityProvider(@PathVariable String id, @RequestBody IdentityProvider body, @RequestParam(required = false, defaultValue = "false") boolean rawConfig) throws MetadataProviderException { body.setSerializeConfigRaw(rawConfig); String zoneId = identityZoneManager.getCurrentIdentityZoneId(); @@ -234,11 +228,11 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str body.setConfig(definition); } - final IdentityProvider updatedIdp = transactionTemplate.execute(txStatus -> { - final IdentityProvider updatedOriginalIdp = identityProviderProvisioning.update(body, zoneId); - return ensureConsistencyOfMirroredIdp(updatedOriginalIdp); - }); - if (updatedIdp == null) { + final IdentityProvider updatedIdp = identityProviderProvisioning.update(body, zoneId); + + // propagate the change to the mirrored IdP if necessary + final IdentityProvider updatedIdpAfterMirrorHandling = ensureConsistencyOfMirroredIdp(updatedIdp); + if (updatedIdpAfterMirrorHandling == null) { logger.warn( "IdentityProvider[origin={}; zone={}] - Transaction updating IdP and mirrored IdP was not successful, but no exception was thrown.", getCleanedUserControlString(body.getOriginKey()), @@ -246,10 +240,10 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str ); return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } - updatedIdp.setSerializeConfigRaw(rawConfig); - redactSensitiveData(updatedIdp); + updatedIdpAfterMirrorHandling.setSerializeConfigRaw(rawConfig); + redactSensitiveData(updatedIdpAfterMirrorHandling); - return new ResponseEntity<>(updatedIdp, OK); + return new ResponseEntity<>(updatedIdpAfterMirrorHandling, OK); } @RequestMapping (value = "{id}/status", method = PATCH) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index 8edd3944fc2..9ab6dc05343 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -56,7 +56,6 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.transaction.PlatformTransactionManager; @ExtendWith(PollutionPreventionExtension.class) @ExtendWith(MockitoExtension.class) @@ -71,9 +70,6 @@ class IdentityProviderEndpointsTest { @Mock private IdentityZoneManager mockIdentityZoneManager; - @Mock - private PlatformTransactionManager mockPlatformTransactionManager; - @Mock private IdentityZoneProvisioning mockIdentityZoneProvisioning; From dbb3063a993b411c846052091cfc6a08fd73b564 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 10 Jan 2024 19:17:04 +0100 Subject: [PATCH 53/91] Fix unit tests --- .../IdentityProviderEndpointsAliasMockMvcTests.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index d3fa16d52ee..068e62f89dc 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -783,10 +783,7 @@ private static AbstractIdentityProviderDefinition buildIdpDefinition(final Strin passwordPolicy.setRequireLowerCaseCharacter(1); passwordPolicy.setRequireSpecialCharacter(1); passwordPolicy.setPasswordNewerThan(new Date(System.currentTimeMillis())); - final UaaIdentityProviderDefinition uaaDefinition = new UaaIdentityProviderDefinition(); - uaaDefinition.setPasswordPolicy(new PasswordPolicy()); - - return uaaDefinition; + return new UaaIdentityProviderDefinition(passwordPolicy, null); default: throw new IllegalArgumentException("IdP type not supported."); } From 625c63a9d0f8149f50cedb42d2433c19411dd172 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 11 Jan 2024 13:39:41 +0100 Subject: [PATCH 54/91] Revert "Replace transaction manager usage with Transactional annotation" This reverts commit 10614d4e41b431d798cf8f61f1237992aa834534. --- .../provider/IdentityProviderEndpoints.java | 40 +++++++++++-------- .../IdentityProviderEndpointsTest.java | 4 ++ 2 files changed, 27 insertions(+), 17 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 8766f256fd9..a0602fbbe39 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 @@ -66,7 +66,9 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -93,6 +95,7 @@ public class IdentityProviderEndpoints implements ApplicationEventPublisherAware private final IdentityProviderConfigValidator configValidator; private final IdentityZoneManager identityZoneManager; private final IdentityZoneProvisioning identityZoneProvisioning; + private final TransactionTemplate transactionTemplate; private ApplicationEventPublisher publisher = null; @@ -108,7 +111,8 @@ public IdentityProviderEndpoints( final @Qualifier("metaDataProviders") SamlIdentityProviderConfigurator samlConfigurator, final @Qualifier("identityProviderConfigValidator") IdentityProviderConfigValidator configValidator, final IdentityZoneManager identityZoneManager, - final @Qualifier("identityZoneProvisioning") IdentityZoneProvisioning identityZoneProvisioning + final @Qualifier("identityZoneProvisioning") IdentityZoneProvisioning identityZoneProvisioning, + final @Qualifier("transactionManager") PlatformTransactionManager transactionManager ) { this.identityProviderProvisioning = identityProviderProvisioning; this.scimGroupExternalMembershipManager = scimGroupExternalMembershipManager; @@ -117,10 +121,10 @@ public IdentityProviderEndpoints( this.configValidator = configValidator; this.identityZoneManager = identityZoneManager; this.identityZoneProvisioning = identityZoneProvisioning; + this.transactionTemplate = new TransactionTemplate(transactionManager); } @RequestMapping(method = POST) - @Transactional public ResponseEntity createIdentityProvider(@RequestBody IdentityProvider body, @RequestParam(required = false, defaultValue = "false") boolean rawConfig) throws MetadataProviderException{ body.setSerializeConfigRaw(rawConfig); String zoneId = identityZoneManager.getCurrentIdentityZoneId(); @@ -144,10 +148,15 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden } // persist IdP and mirror if necessary - final IdentityProvider createdIdpAfterMirrorHandling; + final IdentityProvider createdIdp; try { - final IdentityProvider createdIdp = identityProviderProvisioning.create(body, zoneId); - createdIdpAfterMirrorHandling = ensureConsistencyOfMirroredIdp(createdIdp); + createdIdp = transactionTemplate.execute(txStatus -> { + final IdentityProvider createdOriginalIdp = identityProviderProvisioning.create(body, zoneId); + createdOriginalIdp.setSerializeConfigRaw(rawConfig); + redactSensitiveData(createdOriginalIdp); + + return ensureConsistencyOfMirroredIdp(createdOriginalIdp); + }); } catch (final IdpAlreadyExistsException e) { return new ResponseEntity<>(body, CONFLICT); } catch (final Exception e) { @@ -155,9 +164,7 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden return new ResponseEntity<>(body, INTERNAL_SERVER_ERROR); } - createdIdpAfterMirrorHandling.setSerializeConfigRaw(rawConfig); - redactSensitiveData(createdIdpAfterMirrorHandling); - return new ResponseEntity<>(createdIdpAfterMirrorHandling, CREATED); + return new ResponseEntity<>(createdIdp, CREATED); } @RequestMapping(value = "{id}", method = DELETE) @@ -195,7 +202,6 @@ public ResponseEntity deleteIdentityProvider(@PathVariable Str } @RequestMapping(value = "{id}", method = PUT) - @Transactional public ResponseEntity updateIdentityProvider(@PathVariable String id, @RequestBody IdentityProvider body, @RequestParam(required = false, defaultValue = "false") boolean rawConfig) throws MetadataProviderException { body.setSerializeConfigRaw(rawConfig); String zoneId = identityZoneManager.getCurrentIdentityZoneId(); @@ -228,11 +234,11 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str body.setConfig(definition); } - final IdentityProvider updatedIdp = identityProviderProvisioning.update(body, zoneId); - - // propagate the change to the mirrored IdP if necessary - final IdentityProvider updatedIdpAfterMirrorHandling = ensureConsistencyOfMirroredIdp(updatedIdp); - if (updatedIdpAfterMirrorHandling == null) { + final IdentityProvider updatedIdp = transactionTemplate.execute(txStatus -> { + final IdentityProvider updatedOriginalIdp = identityProviderProvisioning.update(body, zoneId); + return ensureConsistencyOfMirroredIdp(updatedOriginalIdp); + }); + if (updatedIdp == null) { logger.warn( "IdentityProvider[origin={}; zone={}] - Transaction updating IdP and mirrored IdP was not successful, but no exception was thrown.", getCleanedUserControlString(body.getOriginKey()), @@ -240,10 +246,10 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str ); return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } - updatedIdpAfterMirrorHandling.setSerializeConfigRaw(rawConfig); - redactSensitiveData(updatedIdpAfterMirrorHandling); + updatedIdp.setSerializeConfigRaw(rawConfig); + redactSensitiveData(updatedIdp); - return new ResponseEntity<>(updatedIdpAfterMirrorHandling, OK); + return new ResponseEntity<>(updatedIdp, OK); } @RequestMapping (value = "{id}/status", method = PATCH) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index 9ab6dc05343..8edd3944fc2 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -56,6 +56,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.transaction.PlatformTransactionManager; @ExtendWith(PollutionPreventionExtension.class) @ExtendWith(MockitoExtension.class) @@ -70,6 +71,9 @@ class IdentityProviderEndpointsTest { @Mock private IdentityZoneManager mockIdentityZoneManager; + @Mock + private PlatformTransactionManager mockPlatformTransactionManager; + @Mock private IdentityZoneProvisioning mockIdentityZoneProvisioning; From 23ffb2d2eb5b495f914ad93edb1134068cc0aa50 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 12 Jan 2024 11:37:55 +0100 Subject: [PATCH 55/91] Fix position of redactSensitiveData in IdP create endpoint --- .../uaa/provider/IdentityProviderEndpoints.java | 13 ++++++++++--- .../IdentityProviderEndpointsAliasMockMvcTests.java | 6 +++--- 2 files changed, 13 insertions(+), 6 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 a0602fbbe39..12458416821 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 @@ -152,9 +152,6 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden try { createdIdp = transactionTemplate.execute(txStatus -> { final IdentityProvider createdOriginalIdp = identityProviderProvisioning.create(body, zoneId); - createdOriginalIdp.setSerializeConfigRaw(rawConfig); - redactSensitiveData(createdOriginalIdp); - return ensureConsistencyOfMirroredIdp(createdOriginalIdp); }); } catch (final IdpAlreadyExistsException e) { @@ -163,6 +160,16 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden logger.warn("Unable to create IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "]", e); return new ResponseEntity<>(body, INTERNAL_SERVER_ERROR); } + if (createdIdp == null) { + logger.warn( + "IdentityProvider[origin={}; zone={}] - Transaction creating IdP and mirrored IdP was not successful, but no exception was thrown.", + getCleanedUserControlString(body.getOriginKey()), + getCleanedUserControlString(body.getIdentityZoneId()) + ); + return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); + } + createdIdp.setSerializeConfigRaw(rawConfig); + redactSensitiveData(createdIdp); return new ResponseEntity<>(createdIdp, CREATED); } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 068e62f89dc..2cecc56169c 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -188,14 +188,14 @@ void shouldReject_IdpWithOriginAlreadyExistsInAliasZone_UaaToCustomZone() throws } private void shouldReject_IdpWithOriginAlreadyExistsInAliasZone(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - // create IdP with origin key in custom zone + // create IdP with origin key in zone 1 final IdentityProvider createdIdp1 = createIdp( zone1, buildSamlIdpWithAliasProperties(zone1.getId(), null, null) ); assertThat(createdIdp1).isNotNull(); - // then, create an IdP in the "uaa" zone with the same origin key that should be mirrored to the custom zone + // then, create an IdP in zone 2 with the same origin key that should be mirrored to zone 1 -> should fail shouldRejectCreation( zone2, buildIdpWithAliasProperties(zone2.getId(), null, zone1.getId(), createdIdp1.getOriginKey(), SAML), @@ -229,7 +229,7 @@ void shouldAccept_ShouldCreateMirroredIdp_CustomToUaaZone() throws Exception { } private void shouldAccept_ShouldCreateMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - // create regular idp without alias properties in UAA zone + // create regular idp without alias properties in zone 1 final IdentityProvider existingIdpWithoutAlias = createIdp( zone1, buildSamlIdpWithAliasProperties(zone1.getId(), null, null) From 49c413efd0ca5c2dcc6edb0846732a3dac5d9470 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 12 Jan 2024 11:54:24 +0100 Subject: [PATCH 56/91] Change wording from "mirrored" to "alias" in IdentityProviderEndpointsAliasMockMvcTests --- ...ityProviderEndpointsAliasMockMvcTests.java | 208 +++++++++--------- 1 file changed, 104 insertions(+), 104 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 2cecc56169c..eebbd6b75a7 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -93,16 +93,16 @@ void setUp() throws Exception { @Nested class Create { @Test - void shouldAccept_MirrorIdp_UaaToCustomZone() throws Exception { - shouldAccept_MirrorIdp(IdentityZone.getUaa(), customZone); + void shouldAccept_CreateAliasIdp_UaaToCustomZone() throws Exception { + shouldAccept_CreateAliasIdp(IdentityZone.getUaa(), customZone); } @Test - void shouldAccept_MirrorIdp_CustomToUaaZone() throws Exception { - shouldAccept_MirrorIdp(customZone, IdentityZone.getUaa()); + void shouldAccept_CreateAliasIdp_CustomToUaaZone() throws Exception { + shouldAccept_CreateAliasIdp(customZone, IdentityZone.getUaa()); } - private void shouldAccept_MirrorIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + private void shouldAccept_CreateAliasIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { // build IdP in zone1 with aliasZid set to zone2 final IdentityProvider provider = buildSamlIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); @@ -112,15 +112,15 @@ private void shouldAccept_MirrorIdp(final IdentityZone zone1, final IdentityZone assertThat(originalIdp.getAliasId()).isNotBlank(); assertThat(originalIdp.getAliasZid()).isNotBlank().isEqualTo(zone2.getId()); - // read mirrored IdP from zone2 + // read alias IdP from zone2 final String id = originalIdp.getAliasId(); - final Optional> mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id); - assertThat(mirroredIdp).isPresent(); - assertIdpReferencesOtherIdp(mirroredIdp.get(), originalIdp); - assertOtherPropertiesAreEqual(originalIdp, mirroredIdp.get()); + final Optional> aliasIdp = readIdpFromZoneIfExists(zone2.getId(), id); + assertThat(aliasIdp).isPresent(); + assertIdpReferencesOtherIdp(aliasIdp.get(), originalIdp); + assertOtherPropertiesAreEqual(originalIdp, aliasIdp.get()); - // check if aliasId in first IdP is equal to the ID of the mirrored one - assertThat(mirroredIdp.get().getId()).isEqualTo(originalIdp.getAliasId()); + // check if aliasId in first IdP is equal to the ID of the alias IdP + assertThat(aliasIdp.get().getId()).isEqualTo(originalIdp.getAliasId()); } @Test @@ -139,16 +139,16 @@ private void shouldReject_IdzAndAliasZidAreEqual(final IdentityZone zone) throws } @Test - void shouldReject_MirroringNotSupportedForIdpType_UaaToCustomZone() throws Exception { - shouldReject_MirroringNotSupportedForIdpType(IdentityZone.getUaa(), customZone); + void shouldReject_AliasNotSupportedForIdpType_UaaToCustomZone() throws Exception { + shouldReject_AliasNotSupportedForIdpType(IdentityZone.getUaa(), customZone); } @Test - void shouldReject_MirroringNotSupportedForIdpType_CustomToUaaZone() throws Exception { - shouldReject_MirroringNotSupportedForIdpType(customZone, IdentityZone.getUaa()); + void shouldReject_AliasNotSupportedForIdpType_CustomToUaaZone() throws Exception { + shouldReject_AliasNotSupportedForIdpType(customZone, IdentityZone.getUaa()); } - private void shouldReject_MirroringNotSupportedForIdpType(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + private void shouldReject_AliasNotSupportedForIdpType(final IdentityZone zone1, final IdentityZone zone2) throws Exception { final IdentityProvider uaaIdp = buildUaaIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); shouldRejectCreation(zone1, uaaIdp, HttpStatus.UNPROCESSABLE_ENTITY); } @@ -195,7 +195,7 @@ private void shouldReject_IdpWithOriginAlreadyExistsInAliasZone(final IdentityZo ); assertThat(createdIdp1).isNotNull(); - // then, create an IdP in zone 2 with the same origin key that should be mirrored to zone 1 -> should fail + // then, create an IdP in zone 2 with the same origin key for which an alias in zone 1 should be created -> should fail shouldRejectCreation( zone2, buildIdpWithAliasProperties(zone2.getId(), null, zone1.getId(), createdIdp1.getOriginKey(), SAML), @@ -219,16 +219,16 @@ private void shouldRejectCreation(final IdentityZone zone, final IdentityProvide @Nested class Update { @Test - void shouldAccept_ShouldCreateMirroredIdp_UaaToCustomZone() throws Exception { - shouldAccept_ShouldCreateMirroredIdp(IdentityZone.getUaa(), customZone); + void shouldAccept_ShouldCreateAliasIdp_UaaToCustomZone() throws Exception { + shouldAccept_ShouldCreateAliasIdp(IdentityZone.getUaa(), customZone); } @Test - void shouldAccept_ShouldCreateMirroredIdp_CustomToUaaZone() throws Exception { - shouldAccept_ShouldCreateMirroredIdp(customZone, IdentityZone.getUaa()); + void shouldAccept_ShouldCreateAliasIdp_CustomToUaaZone() throws Exception { + shouldAccept_ShouldCreateAliasIdp(customZone, IdentityZone.getUaa()); } - private void shouldAccept_ShouldCreateMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + private void shouldAccept_ShouldCreateAliasIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { // create regular idp without alias properties in zone 1 final IdentityProvider existingIdpWithoutAlias = createIdp( zone1, @@ -244,28 +244,28 @@ private void shouldAccept_ShouldCreateMirroredIdp(final IdentityZone zone1, fina assertThat(idpAfterUpdate.getAliasZid()).isNotBlank(); assertThat(zone2.getId()).isEqualTo(idpAfterUpdate.getAliasZid()); - // read mirrored IdP through alias id in original IdP + // read alias IdP through alias id in original IdP final String id = idpAfterUpdate.getAliasId(); final Optional> idp = readIdpFromZoneIfExists(zone2.getId(), id); assertThat(idp).isPresent(); - final IdentityProvider mirroredIdp = idp.get(); - assertIdpReferencesOtherIdp(mirroredIdp, idpAfterUpdate); - assertOtherPropertiesAreEqual(idpAfterUpdate, mirroredIdp); + final IdentityProvider aliasIdp = idp.get(); + assertIdpReferencesOtherIdp(aliasIdp, idpAfterUpdate); + assertOtherPropertiesAreEqual(idpAfterUpdate, aliasIdp); } @Test - void shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged_UaaToCustomZone() throws Exception { - shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged(IdentityZone.getUaa(), customZone); + void shouldAccept_OtherPropertiesOfIdpWithAliasAreChanged_UaaToCustomZone() throws Exception { + shouldAccept_OtherPropertiesOfIdpWithAliasAreChanged(IdentityZone.getUaa(), customZone); } @Test - void shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged_CustomToUaaZone() throws Exception { - shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged(customZone, IdentityZone.getUaa()); + void shouldAccept_OtherPropertiesOfIdpWithAliasAreChanged_CustomToUaaZone() throws Exception { + shouldAccept_OtherPropertiesOfIdpWithAliasAreChanged(customZone, IdentityZone.getUaa()); } - private void shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - // create a mirrored IdP - final IdentityProvider originalIdp = createMirroredIdp(zone1, zone2); + private void shouldAccept_OtherPropertiesOfIdpWithAliasAreChanged(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + // create an IdP with an alias + final IdentityProvider originalIdp = createIdpWithAlias(zone1, zone2); // update other property final String newName = "new name"; @@ -277,28 +277,28 @@ private void shouldAccept_OtherPropertiesOfAlreadyMirroredIdpAreChanged(final Id assertThat(updatedOriginalIdp.getAliasZid()).isEqualTo(zone2.getId()); assertThat(updatedOriginalIdp.getName()).isNotBlank().isEqualTo(newName); - // check if the change is propagated to the mirrored IdP + // check if the change is propagated to the alias IdP final String id = updatedOriginalIdp.getAliasId(); - final Optional> mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id); - assertThat(mirroredIdp).isPresent(); - assertIdpReferencesOtherIdp(mirroredIdp.get(), updatedOriginalIdp); - assertThat(mirroredIdp.get().getName()).isNotBlank().isEqualTo(newName); + final Optional> aliasIdp = readIdpFromZoneIfExists(zone2.getId(), id); + assertThat(aliasIdp).isPresent(); + assertIdpReferencesOtherIdp(aliasIdp.get(), updatedOriginalIdp); + assertThat(aliasIdp.get().getName()).isNotBlank().isEqualTo(newName); } @Test - void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp_UaaToCustomZone() throws Exception { - shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp(IdentityZone.getUaa(), customZone); + void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewAliasIdp_UaaToCustomZone() throws Exception { + shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewAliasIdp(IdentityZone.getUaa(), customZone); } @Test - void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp_CustomToUaaZone() throws Exception { - shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp(customZone, IdentityZone.getUaa()); + void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewAliasIdp_CustomToUaaZone() throws Exception { + shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewAliasIdp(customZone, IdentityZone.getUaa()); } - private void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final IdentityProvider idp = createMirroredIdp(zone1, zone2); + private void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewAliasIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + final IdentityProvider idp = createIdpWithAlias(zone1, zone2); - // delete the mirrored IdP directly in the DB -> after that, there is a dangling reference + // delete the alias IdP directly in the DB -> after that, there is a dangling reference deleteIdpViaDb(idp.getOriginKey(), zone2.getId()); // update some other property on the original IdP @@ -307,39 +307,39 @@ private void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewMirroredIdp(fi assertThat(updatedIdp.getAliasId()).isNotBlank().isNotEqualTo(idp.getAliasId()); assertThat(updatedIdp.getAliasZid()).isNotBlank().isEqualTo(idp.getAliasZid()); - // check if the new mirrored IdP is present and has the correct properties + // check if the new alias IdP is present and has the correct properties final String id = updatedIdp.getAliasId(); - final Optional> mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), id); - assertThat(mirroredIdp).isPresent(); - assertIdpReferencesOtherIdp(updatedIdp, mirroredIdp.get()); - assertOtherPropertiesAreEqual(updatedIdp, mirroredIdp.get()); + final Optional> aliasIdp = readIdpFromZoneIfExists(zone2.getId(), id); + assertThat(aliasIdp).isPresent(); + assertIdpReferencesOtherIdp(updatedIdp, aliasIdp.get()); + assertOtherPropertiesAreEqual(updatedIdp, aliasIdp.get()); } @ParameterizedTest - @MethodSource("shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp") - void shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp_UaaToCustomZone(final String newAliasId, final String newAliasZid) throws Exception { - shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp(newAliasId, newAliasZid, IdentityZone.getUaa(), customZone); + @MethodSource("shouldReject_ChangingAliasPropertiesOfIdpWithAlias") + void shouldReject_ChangingAliasPropertiesOfIdpWithAlias_UaaToCustomZone(final String newAliasId, final String newAliasZid) throws Exception { + shouldReject_ChangingAliasPropertiesOfIdpWithAlias(newAliasId, newAliasZid, IdentityZone.getUaa(), customZone); } @ParameterizedTest - @MethodSource("shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp") - void shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp_CustomToUaaZone(final String newAliasId, final String newAliasZid) throws Exception { - shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp(newAliasId, newAliasZid, customZone, IdentityZone.getUaa()); + @MethodSource("shouldReject_ChangingAliasPropertiesOfIdpWithAlias") + void shouldReject_ChangingAliasPropertiesOfIdpWithAlias_CustomToUaaZone(final String newAliasId, final String newAliasZid) throws Exception { + shouldReject_ChangingAliasPropertiesOfIdpWithAlias(newAliasId, newAliasZid, customZone, IdentityZone.getUaa()); } - private void shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp( + private void shouldReject_ChangingAliasPropertiesOfIdpWithAlias( final String newAliasId, final String newAliasZid, final IdentityZone zone1, final IdentityZone zone2 ) throws Exception { - final IdentityProvider originalIdp = createMirroredIdp(zone1, zone2); + final IdentityProvider originalIdp = createIdpWithAlias(zone1, zone2); originalIdp.setAliasId(newAliasId); originalIdp.setAliasZid(newAliasZid); shouldRejectUpdate(zone1, originalIdp, HttpStatus.UNPROCESSABLE_ENTITY); } - private static Stream shouldReject_ChangingAliasPropertiesOfAlreadyMirroredIdp() { + private static Stream shouldReject_ChangingAliasPropertiesOfIdpWithAlias() { return Stream.of(null, "", "other").flatMap(aliasIdValue -> Stream.of(null, "", "other").map(aliasZidValue -> Arguments.of(aliasIdValue, aliasZidValue) @@ -365,21 +365,21 @@ private void shouldReject_OnlyAliasIdSet(final IdentityZone zone) throws Excepti } @Test - void shouldReject_MirroringNotSupportedForIdpType_UaaToCustomZone() throws Exception { - shouldReject_MirroringNotSupportedForIdpType(IdentityZone.getUaa(), customZone); + void shouldReject_AliasNotSupportedForIdpType_UaaToCustomZone() throws Exception { + shouldReject_AliasNotSupportedForIdpType(IdentityZone.getUaa(), customZone); } @Test - void shouldReject_MirroringNotSupportedForIdpType_CustomZone() throws Exception { - shouldReject_MirroringNotSupportedForIdpType(customZone, IdentityZone.getUaa()); + void shouldReject_AliasNotSupportedForIdpType_CustomZone() throws Exception { + shouldReject_AliasNotSupportedForIdpType(customZone, IdentityZone.getUaa()); } - private void shouldReject_MirroringNotSupportedForIdpType(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + private void shouldReject_AliasNotSupportedForIdpType(final IdentityZone zone1, final IdentityZone zone2) throws Exception { final IdentityProvider uaaIdp = buildUaaIdpWithAliasProperties(zone1.getId(), null, null); final IdentityProvider createdProvider = createIdp(zone1, uaaIdp); assertThat(createdProvider.getAliasZid()).isBlank(); - // try to mirror the IdP -> should fail because of the IdP's type + // try to create an alias for the IdP -> should fail because of the IdP's type createdProvider.setAliasZid(zone2.getId()); shouldRejectUpdate(zone1, createdProvider, HttpStatus.UNPROCESSABLE_ENTITY); } @@ -415,13 +415,13 @@ private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final Identi } @Test - void shouldReject_IdpInCustomZoneMirroredToOtherCustomZone() throws Exception { + void shouldReject_IdpInCustomZone_AliasToOtherCustomZone() throws Exception { final IdentityProvider idpInCustomZone = createIdp( customZone, buildSamlIdpWithAliasProperties(customZone.getId(), null, null) ); - // try to mirror it to another custom zone + // try to create an alias in another custom zone -> should fail idpInCustomZone.setAliasZid("not-uaa"); shouldRejectUpdate(customZone, idpInCustomZone, HttpStatus.UNPROCESSABLE_ENTITY); } @@ -482,17 +482,17 @@ private void shouldRejectUpdate(final IdentityZone zone, final IdentityProvider< assertThat(idpBeforeUpdateOpt).isPresent(); final IdentityProvider idpBeforeUpdate = idpBeforeUpdateOpt.get(); - // if alias properties set: read mirrored IdP before update - final IdentityProvider mirroredIdpBeforeUpdate; + // if alias properties set: read alias IdP before update + final IdentityProvider aliasIdpBeforeUpdate; if (hasText(idpBeforeUpdate.getAliasId()) && hasText(idpBeforeUpdate.getAliasZid())) { - final Optional> mirroredIdpBeforeUpdateOpt = readIdpFromZoneIfExists( + final Optional> aliasIdpBeforeUpdateOpt = readIdpFromZoneIfExists( idpBeforeUpdate.getAliasZid(), idpBeforeUpdate.getAliasId() ); - assertThat(mirroredIdpBeforeUpdateOpt).isPresent(); - mirroredIdpBeforeUpdate = mirroredIdpBeforeUpdateOpt.get(); + assertThat(aliasIdpBeforeUpdateOpt).isPresent(); + aliasIdpBeforeUpdate = aliasIdpBeforeUpdateOpt.get(); } else { - mirroredIdpBeforeUpdate = null; + aliasIdpBeforeUpdate = null; } // perform the update -> should fail @@ -506,13 +506,13 @@ private void shouldRejectUpdate(final IdentityZone zone, final IdentityProvider< ); assertThat(idpAfterFailedUpdateOpt).isPresent().contains(idpBeforeUpdate); - // if a mirrored IdP was present before update, check if it also remains unchanged - if (mirroredIdpBeforeUpdate != null) { - final Optional> mirroredIdpAfterFailedUpdateOpt = readIdpFromZoneIfExists( + // if an alias IdP was present before update, check if it also remains unchanged + if (aliasIdpBeforeUpdate != null) { + final Optional> aliasIdpAfterFailedUpdateOpt = readIdpFromZoneIfExists( idpBeforeUpdate.getAliasZid(), idpBeforeUpdate.getAliasId() ); - assertThat(mirroredIdpAfterFailedUpdateOpt).isPresent().contains(mirroredIdpBeforeUpdate); + assertThat(aliasIdpAfterFailedUpdateOpt).isPresent().contains(aliasIdpBeforeUpdate); } } } @@ -527,17 +527,17 @@ private void deleteIdpViaDb(final String originKey, final String zoneId) { @Nested class Delete { @Test - void shouldAlsoDeleteMirroredIdp_UaaToCustomZone() throws Exception { - shouldAlsoDeleteMirroredIdp(IdentityZone.getUaa(), customZone); + void shouldAlsoDeleteAliasIdp_UaaToCustomZone() throws Exception { + shouldAlsoDeleteAliasIdp(IdentityZone.getUaa(), customZone); } @Test - void shouldAlsoDeleteMirroredIdp_CustomToUaaZone() throws Exception { - shouldAlsoDeleteMirroredIdp(customZone, IdentityZone.getUaa()); + void shouldAlsoDeleteAliasIdp_CustomToUaaZone() throws Exception { + shouldAlsoDeleteAliasIdp(customZone, IdentityZone.getUaa()); } - private void shouldAlsoDeleteMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final IdentityProvider idpInZone1 = createMirroredIdp(zone1, zone2); + private void shouldAlsoDeleteAliasIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + final IdentityProvider idpInZone1 = createIdpWithAlias(zone1, zone2); final String id = idpInZone1.getId(); assertThat(id).isNotBlank(); final String aliasId = idpInZone1.getAliasId(); @@ -545,11 +545,11 @@ private void shouldAlsoDeleteMirroredIdp(final IdentityZone zone1, final Identit final String aliasZid = idpInZone1.getAliasZid(); assertThat(aliasZid).isNotBlank().isEqualTo(zone2.getId()); - // check if mirrored IdP is available in zone 2 - final Optional> mirroredIdp = readIdpFromZoneIfExists(zone2.getId(), aliasId); - assertThat(mirroredIdp).isPresent(); - assertThat(mirroredIdp.get().getAliasId()).isNotBlank().isEqualTo(id); - assertThat(mirroredIdp.get().getAliasZid()).isNotBlank().isEqualTo(idpInZone1.getIdentityZoneId()); + // check if alias IdP is available in zone 2 + final Optional> aliasIdp = readIdpFromZoneIfExists(zone2.getId(), aliasId); + assertThat(aliasIdp).isPresent(); + assertThat(aliasIdp.get().getAliasId()).isNotBlank().isEqualTo(id); + assertThat(aliasIdp.get().getAliasZid()).isNotBlank().isEqualTo(idpInZone1.getIdentityZoneId()); // delete IdP in zone 1 final MvcResult deleteResult = deleteIdpAndReturnResult(zone1, id); @@ -560,19 +560,19 @@ private void shouldAlsoDeleteMirroredIdp(final IdentityZone zone1, final Identit } @Test - void shouldIgnoreDanglingReferenceToMirroredIdp_UaaToCustomZone() throws Exception { - shouldIgnoreDanglingReferenceToMirroredIdp(IdentityZone.getUaa(), customZone); + void shouldIgnoreDanglingReferenceToAliasIdp_UaaToCustomZone() throws Exception { + shouldIgnoreDanglingReferenceToAliasIdp(IdentityZone.getUaa(), customZone); } @Test - void shouldIgnoreDanglingReferenceToMirroredIdp_CustomToUaaZone() throws Exception { - shouldIgnoreDanglingReferenceToMirroredIdp(customZone, IdentityZone.getUaa()); + void shouldIgnoreDanglingReferenceToAliasIdp_CustomToUaaZone() throws Exception { + shouldIgnoreDanglingReferenceToAliasIdp(customZone, IdentityZone.getUaa()); } - private void shouldIgnoreDanglingReferenceToMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final IdentityProvider originalIdp = createMirroredIdp(zone1, zone2); + private void shouldIgnoreDanglingReferenceToAliasIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + final IdentityProvider originalIdp = createIdpWithAlias(zone1, zone2); - // create a dangling reference by deleting the mirrored IdP directly in the DB + // create a dangling reference by deleting the alias IdP directly in the DB deleteIdpViaDb(originalIdp.getOriginKey(), zone2.getId()); // delete the original IdP -> dangling reference should be ignored @@ -604,21 +604,21 @@ private void assertIdpReferencesOtherIdp(final IdentityProvider idp, final Id assertThat(referencedIdp.getIdentityZoneId()).isNotBlank().isEqualTo(idp.getAliasZid()); } - private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final IdentityProvider mirroredIdp) { + private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final IdentityProvider aliasIdp) { // apart from the zone ID, the configs should be identical final SamlIdentityProviderDefinition originalIdpConfig = (SamlIdentityProviderDefinition) idp.getConfig(); originalIdpConfig.setZoneId(null); - final SamlIdentityProviderDefinition mirroredIdpConfig = (SamlIdentityProviderDefinition) mirroredIdp.getConfig(); - mirroredIdpConfig.setZoneId(null); - assertThat(mirroredIdpConfig).isEqualTo(originalIdpConfig); + final SamlIdentityProviderDefinition aliasIdpConfig = (SamlIdentityProviderDefinition) aliasIdp.getConfig(); + aliasIdpConfig.setZoneId(null); + assertThat(aliasIdpConfig).isEqualTo(originalIdpConfig); // check if remaining properties are equal - assertThat(mirroredIdp.getOriginKey()).isEqualTo(idp.getOriginKey()); - assertThat(mirroredIdp.getName()).isEqualTo(idp.getName()); - assertThat(mirroredIdp.getType()).isEqualTo(idp.getType()); + assertThat(aliasIdp.getOriginKey()).isEqualTo(idp.getOriginKey()); + assertThat(aliasIdp.getName()).isEqualTo(idp.getName()); + assertThat(aliasIdp.getType()).isEqualTo(idp.getType()); } - private IdentityProvider createMirroredIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + private IdentityProvider createIdpWithAlias(final IdentityZone zone1, final IdentityZone zone2) throws Exception { final IdentityProvider provider = buildSamlIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); final IdentityProvider createdOriginalIdp = createIdp(zone1, provider); assertThat(createdOriginalIdp.getAliasId()).isNotBlank(); From 82bec21b63443c9c6a8959ae83507da0903b9cad Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 12 Jan 2024 12:03:03 +0100 Subject: [PATCH 57/91] Change wording from "mirrored" to "alias" in IdentityProviderEndpoints --- .../provider/IdentityProviderEndpoints.java | 116 +++++++++--------- .../uaa/provider/IdpAliasFailedException.java | 9 ++ .../provider/IdpMirroringFailedException.java | 9 -- 3 files changed, 67 insertions(+), 67 deletions(-) create mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java delete mode 100644 server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpMirroringFailedException.java 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 12458416821..e5e1d2ad11a 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 @@ -83,9 +83,9 @@ public class IdentityProviderEndpoints implements ApplicationEventPublisherAware protected static Logger logger = LoggerFactory.getLogger(IdentityProviderEndpoints.class); /** - * The IdP types for which mirroring via 'alias_id' and 'alias_zid' is supported. + * The IdP types for which alias IdPs (via 'alias_id' and 'alias_zid') are supported. */ - private static final Set IDP_TYPES_MIRRORING_SUPPORTED = Set.of(SAML, OAUTH20, OIDC10); + private static final Set IDP_TYPES_ALIAS_SUPPORTED = Set.of(SAML, OAUTH20, OIDC10); private final IdentityProviderProvisioning identityProviderProvisioning; private final ScimGroupExternalMembershipManager scimGroupExternalMembershipManager; @@ -147,12 +147,12 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden return new ResponseEntity<>(body, UNPROCESSABLE_ENTITY); } - // persist IdP and mirror if necessary + // persist IdP and create alias if necessary final IdentityProvider createdIdp; try { createdIdp = transactionTemplate.execute(txStatus -> { final IdentityProvider createdOriginalIdp = identityProviderProvisioning.create(body, zoneId); - return ensureConsistencyOfMirroredIdp(createdOriginalIdp); + return ensureConsistencyOfAliasIdp(createdOriginalIdp); }); } catch (final IdpAlreadyExistsException e) { return new ResponseEntity<>(body, CONFLICT); @@ -162,7 +162,7 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden } if (createdIdp == null) { logger.warn( - "IdentityProvider[origin={}; zone={}] - Transaction creating IdP and mirrored IdP was not successful, but no exception was thrown.", + "IdentityProvider[origin={}; zone={}] - Transaction creating IdP (and alias IdP, if applicable) was not successful, but no exception was thrown.", getCleanedUserControlString(body.getOriginKey()), getCleanedUserControlString(body.getIdentityZoneId()) ); @@ -190,15 +190,15 @@ public ResponseEntity deleteIdentityProvider(@PathVariable Str publisher.publishEvent(new EntityDeletedEvent<>(existing, authentication, identityZoneId)); redactSensitiveData(existing); - // delete mirrored IdP if alias fields are set + // delete alias IdP if alias fields are set if (hasText(existing.getAliasZid()) && hasText(existing.getAliasId())) { - final IdentityProvider mirroredIdp = retrieveMirroredIdp(existing); - if (mirroredIdp != null) { - mirroredIdp.setSerializeConfigRaw(rawConfig); - publisher.publishEvent(new EntityDeletedEvent<>(mirroredIdp, authentication, identityZoneId)); + final IdentityProvider aliasIdp = retrieveAliasIdp(existing); + if (aliasIdp != null) { + aliasIdp.setSerializeConfigRaw(rawConfig); + publisher.publishEvent(new EntityDeletedEvent<>(aliasIdp, authentication, identityZoneId)); } else { logger.warn( - "Mirrored IdP referenced in IdentityProvider[origin={}; zone={}}] not found, skipping deletion of mirrored IdP.", + "Alias IdP referenced in IdentityProvider[origin={}; zone={}}] not found, skipping deletion of alias IdP.", existing.getOriginKey(), existing.getIdentityZoneId() ); @@ -225,7 +225,7 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str if (!aliasPropertiesAreValid(body, existing)) { logger.warn( - "IdentityProvider[origin={}; zone={}] - Alias ID and/or ZID changed during update of already mirrored IdP.", + "IdentityProvider[origin={}; zone={}] - Alias ID and/or ZID changed during update of IdP with alias.", getCleanedUserControlString(body.getOriginKey()), getCleanedUserControlString(body.getIdentityZoneId()) ); @@ -243,11 +243,11 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str final IdentityProvider updatedIdp = transactionTemplate.execute(txStatus -> { final IdentityProvider updatedOriginalIdp = identityProviderProvisioning.update(body, zoneId); - return ensureConsistencyOfMirroredIdp(updatedOriginalIdp); + return ensureConsistencyOfAliasIdp(updatedOriginalIdp); }); if (updatedIdp == null) { logger.warn( - "IdentityProvider[origin={}; zone={}] - Transaction updating IdP and mirrored IdP was not successful, but no exception was thrown.", + "IdentityProvider[origin={}; zone={}] - Transaction updating IdP (and alias IdP, if applicable) was not successful, but no exception was thrown.", getCleanedUserControlString(body.getOriginKey()), getCleanedUserControlString(body.getIdentityZoneId()) ); @@ -280,8 +280,8 @@ public ResponseEntity updateIdentityProviderStatus(@Path identityProviderProvisioning.update(existing, zoneId); logger.info("PasswordChangeRequired property set for Identity Provider: " + existing.getId()); - /* since this operation is only allowed for IdPs of type "UAA" and mirroring is not supported for "UAA" IdPs, - * we do not need to propagate the changes to a mirrored IdP here. */ + /* since this operation is only allowed for IdPs of type "UAA" and aliases are not supported for "UAA" IdPs, + * we do not need to propagate the changes to an alias IdP here. */ logger.info("PasswordChangeRequired property set for Identity Provider: {}", existing.getId()); return new ResponseEntity<>(body, OK); @@ -345,9 +345,9 @@ private boolean aliasPropertiesAreValid( @NonNull final IdentityProvider requestBody, @Nullable final IdentityProvider existingIdp ) { - // if the IdP was already mirrored, the alias properties must not be changed - final boolean idpWasAlreadyMirrored = existingIdp != null && hasText(existingIdp.getAliasZid()); - if (idpWasAlreadyMirrored) { + // if the IdP already has an alias, the alias properties must not be changed + final boolean idpAlreadyHasAlias = existingIdp != null && hasText(existingIdp.getAliasZid()); + if (idpAlreadyHasAlias) { if (!hasText(existingIdp.getAliasId())) { // at this point, we expect both properties to be set -> if not, the IdP is in an inconsistent state throw new IllegalStateException(String.format( @@ -362,18 +362,18 @@ private boolean aliasPropertiesAreValid( && existingIdp.getAliasZid().equals(requestBody.getAliasZid()); } - // if the IdP was not mirrored already, the aliasId must be empty + // if the IdP does not have an alias already, the aliasId must be empty if (hasText(requestBody.getAliasId())) { return false; } - // check if mirroring is necessary + // check if the creation of an alias is necessary if (!hasText(requestBody.getAliasZid())) { return true; } - // check if mirroring is supported for this IdP type - if (!IDP_TYPES_MIRRORING_SUPPORTED.contains(requestBody.getType())) { + // check if aliases are supported for this IdP type + if (!IDP_TYPES_ALIAS_SUPPORTED.contains(requestBody.getType())) { return false; } @@ -399,51 +399,51 @@ private boolean aliasPropertiesAreValid( } /** - * Ensure consistency during create or update operations with a mirrored IdP referenced in the original IdPs alias - * properties. If the IdP has both its alias ID and alias ZID set, the existing mirrored IdP is updated. If only - * the alias ZID is set, a new mirrored IdP is created. + * Ensure consistency during create or update operations with an alias IdP referenced in the original IdPs alias + * properties. If the IdP has both its alias ID and alias ZID set, the existing alias IdP is updated. If only + * the alias ZID is set, a new alias IdP is created. * This method should be executed in a transaction together with the original create or update operation. * * @param originalIdp the original IdP; must be persisted, i.e., have an ID, already * @return the original IdP after the operation, with a potentially updated "aliasId" field - * @throws IdpMirroringFailedException if a new mirrored IdP needs to be created, but the zone referenced in - * 'aliasZid' does not exist - * @throws IdpMirroringFailedException if 'aliasId' and 'aliasZid' are set in the original IdP, but the referenced - * mirrored IdP could not be found + * @throws IdpAliasFailedException if a new alias IdP needs to be created, but the zone referenced in 'aliasZid' + * does not exist + * @throws IdpAliasFailedException if 'aliasId' and 'aliasZid' are set in the original IdP, but the referenced + * alias IdP could not be found */ - private IdentityProvider ensureConsistencyOfMirroredIdp( + private IdentityProvider ensureConsistencyOfAliasIdp( final IdentityProvider originalIdp - ) throws IdpMirroringFailedException { + ) throws IdpAliasFailedException { if (!hasText(originalIdp.getAliasZid())) { - // no mirroring is necessary + // no alias creation/update is necessary return originalIdp; } - final IdentityProvider mirroredIdp = new IdentityProvider<>(); - mirroredIdp.setActive(originalIdp.isActive()); - mirroredIdp.setName(originalIdp.getName()); - mirroredIdp.setOriginKey(originalIdp.getOriginKey()); - mirroredIdp.setType(originalIdp.getType()); - mirroredIdp.setConfig(originalIdp.getConfig()); - mirroredIdp.setSerializeConfigRaw(originalIdp.isSerializeConfigRaw()); + final IdentityProvider aliasIdp = new IdentityProvider<>(); + aliasIdp.setActive(originalIdp.isActive()); + aliasIdp.setName(originalIdp.getName()); + aliasIdp.setOriginKey(originalIdp.getOriginKey()); + aliasIdp.setType(originalIdp.getType()); + aliasIdp.setConfig(originalIdp.getConfig()); + aliasIdp.setSerializeConfigRaw(originalIdp.isSerializeConfigRaw()); // reference the ID and zone ID of the initial IdP entry - mirroredIdp.setAliasZid(originalIdp.getIdentityZoneId()); - mirroredIdp.setAliasId(originalIdp.getId()); - mirroredIdp.setIdentityZoneId(originalIdp.getAliasZid()); + aliasIdp.setAliasZid(originalIdp.getIdentityZoneId()); + aliasIdp.setAliasId(originalIdp.getId()); + aliasIdp.setIdentityZoneId(originalIdp.getAliasZid()); - // get the referenced, mirrored IdP - final IdentityProvider existingMirroredIdp; + // get the referenced alias IdP + final IdentityProvider existingAliasIdp; if (hasText(originalIdp.getAliasId())) { // if the referenced IdP does not exist, we create a new one - existingMirroredIdp = retrieveMirroredIdp(originalIdp); + existingAliasIdp = retrieveAliasIdp(originalIdp); } else { - existingMirroredIdp = null; + existingAliasIdp = null; } - // update the existing mirrored IdP - if (existingMirroredIdp != null) { - mirroredIdp.setId(existingMirroredIdp.getId()); - identityProviderProvisioning.update(mirroredIdp, originalIdp.getAliasZid()); + // update the existing alias IdP + if (existingAliasIdp != null) { + aliasIdp.setId(existingAliasIdp.getId()); + identityProviderProvisioning.update(aliasIdp, originalIdp.getAliasZid()); return originalIdp; } @@ -451,25 +451,25 @@ private IdentityProvider ensur try { identityZoneProvisioning.retrieve(originalIdp.getAliasZid()); } catch (final ZoneDoesNotExistsException e) { - throw new IdpMirroringFailedException(String.format( - "Could not mirror IdP '%s' to zone '%s', as zone does not exist.", + throw new IdpAliasFailedException(String.format( + "Could not create alias for IdP '%s' in zone '%s', as zone does not exist.", originalIdp.getId(), originalIdp.getAliasZid() ), e); } - // create new mirrored IdP in alias zid - final IdentityProvider persistedMirroredIdp = identityProviderProvisioning.create( - mirroredIdp, + // create new alias IdP in alias zid + final IdentityProvider persistedAliasIdp = identityProviderProvisioning.create( + aliasIdp, originalIdp.getAliasZid() ); // update alias ID in original IdP - originalIdp.setAliasId(persistedMirroredIdp.getId()); + originalIdp.setAliasId(persistedAliasIdp.getId()); return identityProviderProvisioning.update(originalIdp, originalIdp.getIdentityZoneId()); } - private IdentityProvider retrieveMirroredIdp(final IdentityProvider originalIdp) { + private IdentityProvider retrieveAliasIdp(final IdentityProvider originalIdp) { try { return identityProviderProvisioning.retrieve( originalIdp.getAliasId(), diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java new file mode 100644 index 00000000000..3d4f7759d7c --- /dev/null +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java @@ -0,0 +1,9 @@ +package org.cloudfoundry.identity.uaa.provider; + +import org.cloudfoundry.identity.uaa.error.UaaException; + +public class IdpAliasFailedException extends UaaException { + public IdpAliasFailedException(final String msg, final Throwable t) { + super(msg, t); + } +} diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpMirroringFailedException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpMirroringFailedException.java deleted file mode 100644 index cb3d0b13e71..00000000000 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpMirroringFailedException.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.cloudfoundry.identity.uaa.provider; - -import org.cloudfoundry.identity.uaa.error.UaaException; - -public class IdpMirroringFailedException extends UaaException { - public IdpMirroringFailedException(final String msg, final Throwable t) { - super(msg, t); - } -} From 83aaf86a6cd5db447a136750c1b9ee25c0cd0d81 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 12 Jan 2024 12:04:47 +0100 Subject: [PATCH 58/91] Change wording from "mirrored" to "alias" in JdbcIdentityProviderProvisioningTests --- ...JdbcIdentityProviderProvisioningTests.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java index f1c463bd379..d780509e708 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java @@ -67,41 +67,41 @@ void deleteProvidersInZone() { } @Test - void deleteProvidersInUaaZone_Mirrored() { + void deleteProvidersInUaaZone_WithAlias() { final IdentityZone mockIdentityZone = mock(IdentityZone.class); when(mockIdentityZone.getId()).thenReturn(otherZoneId1); final String originSuffix = RandomStringUtils.randomAlphabetic(5); - // IdP 1: not mirrored + // IdP 1: no alias final IdentityProvider idp1 = MultitenancyFixture.identityProvider("origin1-" + originSuffix, otherZoneId1); final IdentityProvider createdIdp1 = jdbcIdentityProviderProvisioning.create(idp1, otherZoneId1); Assertions.assertThat(createdIdp1).isNotNull(); Assertions.assertThat(createdIdp1.getId()).isNotBlank(); - // IdP 2: mirrored to UAA zone + // IdP 2: alias in UAA zone final String idp2Id = UUID.randomUUID().toString(); - final String idp2MirroredId = UUID.randomUUID().toString(); + final String idp2AliasId = UUID.randomUUID().toString(); final String origin2 = "origin2-" + originSuffix; final IdentityProvider idp2 = MultitenancyFixture.identityProvider(origin2, otherZoneId1); idp2.setId(idp2Id); idp2.setAliasZid(uaaZoneId); - idp2.setAliasId(idp2MirroredId); + idp2.setAliasId(idp2AliasId); final IdentityProvider createdIdp2 = jdbcIdentityProviderProvisioning.create(idp2, otherZoneId1); Assertions.assertThat(createdIdp2).isNotNull(); Assertions.assertThat(createdIdp2.getId()).isNotBlank(); - final IdentityProvider idp2Mirrored = MultitenancyFixture.identityProvider(origin2, uaaZoneId); - idp2Mirrored.setId(idp2MirroredId); - idp2Mirrored.setAliasZid(otherZoneId1); - idp2Mirrored.setAliasId(idp2Id); - final IdentityProvider createdIdp2Mirrored = jdbcIdentityProviderProvisioning.create(idp2Mirrored, uaaZoneId); - Assertions.assertThat(createdIdp2Mirrored).isNotNull(); - Assertions.assertThat(createdIdp2Mirrored.getId()).isNotBlank(); + final IdentityProvider idp2Alias = MultitenancyFixture.identityProvider(origin2, uaaZoneId); + idp2Alias.setId(idp2AliasId); + idp2Alias.setAliasZid(otherZoneId1); + idp2Alias.setAliasId(idp2Id); + final IdentityProvider createdIdp2Alias = jdbcIdentityProviderProvisioning.create(idp2Alias, uaaZoneId); + Assertions.assertThat(createdIdp2Alias).isNotNull(); + Assertions.assertThat(createdIdp2Alias.getId()).isNotBlank(); // check if all three entries are present in the DB Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{otherZoneId1, createdIdp1.getId()}, Integer.class)).isEqualTo(1); Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{otherZoneId1, createdIdp2.getId()}, Integer.class)).isEqualTo(1); - Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{uaaZoneId, createdIdp2Mirrored.getId()}, Integer.class)).isEqualTo(1); + Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{uaaZoneId, createdIdp2Alias.getId()}, Integer.class)).isEqualTo(1); // emit custom zone deleted event jdbcIdentityProviderProvisioning.onApplicationEvent(new EntityDeletedEvent<>(mockIdentityZone, null, otherZoneId1)); @@ -109,7 +109,7 @@ void deleteProvidersInUaaZone_Mirrored() { // check if all three entries are gone Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{otherZoneId1, createdIdp1.getId()}, Integer.class)).isZero(); Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{otherZoneId1, createdIdp2.getId()}, Integer.class)).isZero(); - Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{uaaZoneId, createdIdp2Mirrored.getId()}, Integer.class)).isZero(); + Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{uaaZoneId, createdIdp2Alias.getId()}, Integer.class)).isZero(); } @Test From d75de2d6ac67207d4addbbac976d3038eb0872d0 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 12 Jan 2024 12:06:04 +0100 Subject: [PATCH 59/91] Change wording from "mirrored" to "alias" in IdentityProviderTest --- .../identity/uaa/provider/IdentityProviderTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java b/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java index a59fedd2c62..0e7c402281e 100644 --- a/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java +++ b/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java @@ -14,27 +14,27 @@ void testToString_ShouldContainAliasProperties() { idp.setName("some-name"); idp.setOriginKey("some-origin"); idp.setAliasZid("custom-zone"); - idp.setAliasId("id-of-mirrored-idp"); + idp.setAliasId("id-of-alias-idp"); idp.setActive(true); idp.setIdentityZoneId(UAA); final OIDCIdentityProviderDefinition config = new OIDCIdentityProviderDefinition(); config.setIssuer("issuer"); idp.setConfig(config); - assertThat(idp).hasToString("IdentityProvider{id='12345', identityZoneId='uaa', originKey='some-origin', name='some-name', type='oidc1.0', active=true, aliasId='id-of-mirrored-idp', aliasZid='custom-zone'}"); + assertThat(idp).hasToString("IdentityProvider{id='12345', identityZoneId='uaa', originKey='some-origin', name='some-name', type='oidc1.0', active=true, aliasId='id-of-alias-idp', aliasZid='custom-zone'}"); } @Test void testEqualsAndHashCode() { final String customZoneId = "custom-zone"; - final String mirroredIdpId = "id-of-mirrored-idp"; + final String aliasIdpId = "id-of-alias-idp"; final IdentityProvider idp1 = new IdentityProvider<>(); idp1.setId("12345"); idp1.setName("some-name"); idp1.setOriginKey("some-origin"); idp1.setAliasZid(customZoneId); - idp1.setAliasId(mirroredIdpId); + idp1.setAliasId(aliasIdpId); idp1.setActive(true); idp1.setIdentityZoneId(UAA); final OIDCIdentityProviderDefinition config1 = new OIDCIdentityProviderDefinition(); @@ -46,7 +46,7 @@ void testEqualsAndHashCode() { idp2.setName("some-name"); idp2.setOriginKey("some-origin"); idp2.setAliasZid(customZoneId); - idp2.setAliasId(mirroredIdpId); + idp2.setAliasId(aliasIdpId); idp2.setActive(true); idp2.setIdentityZoneId(UAA); final OIDCIdentityProviderDefinition config2 = new OIDCIdentityProviderDefinition(); From 438e7c3939400e6e6064e9d790d3ae36ee6da57e Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 12 Jan 2024 12:10:08 +0100 Subject: [PATCH 60/91] Change wording from "mirrored" to "alias" in IdentityProviderEndpointsTest --- .../IdentityProviderEndpointsTest.java | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index 8edd3944fc2..fd2871cd9ef 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -342,20 +342,20 @@ void update_ldap_provider_takes_new_password() throws Exception { } @Test - void testUpdateIdentityProvider_AlreadyMirrored_InvalidAliasPropertyChange() throws MetadataProviderException { + void testUpdateIdpWithExistingAlias_InvalidAliasPropertyChange() throws MetadataProviderException { final String existingIdpId = UUID.randomUUID().toString(); final String customZoneId = UUID.randomUUID().toString(); - final String mirroredIdpId = UUID.randomUUID().toString(); + final String aliasIdpId = UUID.randomUUID().toString(); final Supplier> existingIdpSupplier = () -> { final IdentityProvider idp = getExternalOAuthProvider(); idp.setId(existingIdpId); idp.setAliasZid(customZoneId); - idp.setAliasId(mirroredIdpId); + idp.setAliasId(aliasIdpId); return idp; }; - // original IdP with reference to a mirrored IdP + // original IdP with reference to an alias IdP final IdentityProvider existingIdp = existingIdpSupplier.get(); when(mockIdentityProviderProvisioning.retrieve(existingIdpId, IdentityZone.getUaaZoneId())) .thenReturn(existingIdp); @@ -386,10 +386,10 @@ void testUpdateIdentityProvider_AlreadyMirrored_InvalidAliasPropertyChange() thr } @Test - void testUpdateIdentityProvider_AlreadyMirrored_ValidChange() throws MetadataProviderException { + void testUpdateIdpWithExistingAlias_ValidChange() throws MetadataProviderException { final String existingIdpId = UUID.randomUUID().toString(); final String customZoneId = UUID.randomUUID().toString(); - final String mirroredIdpId = UUID.randomUUID().toString(); + final String aliasIdpId = UUID.randomUUID().toString(); when(mockIdentityZoneManager.getCurrentIdentityZoneId()).thenReturn(UAA); @@ -397,18 +397,18 @@ void testUpdateIdentityProvider_AlreadyMirrored_ValidChange() throws MetadataPro final IdentityProvider idp = getExternalOAuthProvider(); idp.setId(existingIdpId); idp.setAliasZid(customZoneId); - idp.setAliasId(mirroredIdpId); + idp.setAliasId(aliasIdpId); return idp; }; final IdentityProvider existingIdp = existingIdpSupplier.get(); when(mockIdentityProviderProvisioning.retrieve(existingIdpId, UAA)).thenReturn(existingIdp); - final IdentityProvider mirroredIdp = getExternalOAuthProvider(); - mirroredIdp.setId(mirroredIdpId); - mirroredIdp.setIdentityZoneId(customZoneId); - mirroredIdp.setAliasId(existingIdp.getId()); - mirroredIdp.setAliasZid(UAA); - when(mockIdentityProviderProvisioning.retrieve(mirroredIdpId, customZoneId)).thenReturn(mirroredIdp); + final IdentityProvider aliasIdp = getExternalOAuthProvider(); + aliasIdp.setId(aliasIdpId); + aliasIdp.setIdentityZoneId(customZoneId); + aliasIdp.setAliasId(existingIdp.getId()); + aliasIdp.setAliasZid(UAA); + when(mockIdentityProviderProvisioning.retrieve(aliasIdpId, customZoneId)).thenReturn(aliasIdp); when(mockIdentityProviderProvisioning.update(any(), anyString())) .thenAnswer(invocation -> invocation.getArgument(0)); @@ -431,10 +431,10 @@ void testUpdateIdentityProvider_AlreadyMirrored_ValidChange() throws MetadataPro Assertions.assertThat(firstIdp.getId()).isEqualTo(existingIdpId); Assertions.assertThat(firstIdp.getName()).isEqualTo(newName); - // expecting mirrored IdP with the new name + // expecting alias IdP with the new name final IdentityProvider secondIdp = idpArgumentCaptor.getAllValues().get(1); Assertions.assertThat(secondIdp).isNotNull(); - Assertions.assertThat(secondIdp.getId()).isEqualTo(mirroredIdpId); + Assertions.assertThat(secondIdp.getId()).isEqualTo(aliasIdpId); Assertions.assertThat(secondIdp.getName()).isEqualTo(newName); } @@ -510,22 +510,22 @@ void testCreateIdentityProvider_ValidAliasProperties() throws MetadataProviderEx createdOriginalIdp.setId(originalIdpId); final IdpWithAliasMatcher requestBodyMatcher = new IdpWithAliasMatcher(UAA, null, null, customZoneId); - // idpProvisioning.create should add ID to mirrored IdP - final IdentityProvider persistedMirroredIdp = requestBodyProvider.get(); - final String mirroredIdpId = UUID.randomUUID().toString(); - persistedMirroredIdp.setAliasId(originalIdpId); - persistedMirroredIdp.setAliasZid(UAA); - persistedMirroredIdp.setIdentityZoneId(customZoneId); - persistedMirroredIdp.setId(mirroredIdpId); - final IdpWithAliasMatcher mirroredIdpMatcher = new IdpWithAliasMatcher(customZoneId, null, originalIdpId, UAA); + // idpProvisioning.create should add ID to alias IdP + final IdentityProvider persistedAliasIdp = requestBodyProvider.get(); + final String aliasIdpId = UUID.randomUUID().toString(); + persistedAliasIdp.setAliasId(originalIdpId); + persistedAliasIdp.setAliasZid(UAA); + persistedAliasIdp.setIdentityZoneId(customZoneId); + persistedAliasIdp.setId(aliasIdpId); + final IdpWithAliasMatcher aliasIdpMatcher = new IdpWithAliasMatcher(customZoneId, null, originalIdpId, UAA); when(mockIdentityProviderProvisioning.create(any(), anyString())).thenAnswer(invocation -> { final IdentityProvider idp = invocation.getArgument(0); final String idzId = invocation.getArgument(1); if (requestBodyMatcher.matches(idp) && idzId.equals(UAA)) { return createdOriginalIdp; } - if (mirroredIdpMatcher.matches(idp) && idzId.equals(customZoneId)) { - return persistedMirroredIdp; + if (aliasIdpMatcher.matches(idp) && idzId.equals(customZoneId)) { + return persistedAliasIdp; } return null; }); @@ -533,9 +533,9 @@ void testCreateIdentityProvider_ValidAliasProperties() throws MetadataProviderEx // mock idpProvisioning.update final IdentityProvider createdOriginalIdpWithAliasId = requestBodyProvider.get(); createdOriginalIdpWithAliasId.setId(originalIdpId); - createdOriginalIdpWithAliasId.setAliasId(mirroredIdpId); + createdOriginalIdpWithAliasId.setAliasId(aliasIdpId); when(mockIdentityProviderProvisioning.update( - argThat(new IdpWithAliasMatcher(UAA, originalIdpId, mirroredIdpId, customZoneId)), + argThat(new IdpWithAliasMatcher(UAA, originalIdpId, aliasIdpId, customZoneId)), eq(UAA) )).thenReturn(createdOriginalIdpWithAliasId); @@ -665,26 +665,26 @@ void testDeleteIdentityProviderExisting() { } @Test - void testDeleteIdentityProviderMirrored() { + void testDeleteIdpWithAlias() { final String idpId = UUID.randomUUID().toString(); - final String mirroredIdpId = UUID.randomUUID().toString(); + final String aliasIdpId = UUID.randomUUID().toString(); final String customZoneId = UUID.randomUUID().toString(); final IdentityProvider idp = new IdentityProvider<>(); idp.setType(OIDC10); idp.setId(idpId); idp.setIdentityZoneId(UAA); - idp.setAliasId(mirroredIdpId); + idp.setAliasId(aliasIdpId); idp.setAliasZid(customZoneId); when(mockIdentityProviderProvisioning.retrieve(idpId, UAA)).thenReturn(idp); - final IdentityProvider mirroredIdp = new IdentityProvider<>(); - mirroredIdp.setType(OIDC10); - mirroredIdp.setId(mirroredIdpId); - mirroredIdp.setIdentityZoneId(customZoneId); - mirroredIdp.setAliasId(idpId); - mirroredIdp.setAliasZid(UAA); - when(mockIdentityProviderProvisioning.retrieve(mirroredIdpId, customZoneId)).thenReturn(mirroredIdp); + final IdentityProvider aliasIdp = new IdentityProvider<>(); + aliasIdp.setType(OIDC10); + aliasIdp.setId(aliasIdpId); + aliasIdp.setIdentityZoneId(customZoneId); + aliasIdp.setAliasId(idpId); + aliasIdp.setAliasZid(UAA); + when(mockIdentityProviderProvisioning.retrieve(aliasIdpId, customZoneId)).thenReturn(aliasIdp); final ApplicationEventPublisher mockEventPublisher = mock(ApplicationEventPublisher.class); identityProviderEndpoints.setApplicationEventPublisher(mockEventPublisher); @@ -702,7 +702,7 @@ void testDeleteIdentityProviderMirrored() { final EntityDeletedEvent secondEvent = entityDeletedEventCaptor.getAllValues().get(1); Assertions.assertThat(secondEvent).isNotNull(); Assertions.assertThat(secondEvent.getIdentityZoneId()).isEqualTo(UAA); - Assertions.assertThat(((IdentityProvider) secondEvent.getSource()).getId()).isEqualTo(mirroredIdpId); + Assertions.assertThat(((IdentityProvider) secondEvent.getSource()).getId()).isEqualTo(aliasIdpId); } @Test From 8bb24ebfebb690179b72b55e4b481ddcf661fcd5 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 12 Jan 2024 12:11:14 +0100 Subject: [PATCH 61/91] Change wording from "mirrored" to "alias" in IdentityProviderEndpointDocs --- .../uaa/mock/providers/IdentityProviderEndpointDocs.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java index 850e6fb8346..385cb899fe8 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java @@ -79,8 +79,8 @@ class IdentityProviderEndpointDocs extends EndpointDocs { private static final String FAMILY_NAME_DESC = "Map `family_name` to the attribute for family name in the provider assertion or token."; private static final String PHONE_NUMBER_DESC = "Map `phone_number` to the attribute for phone number in the provider assertion or token."; private static final String GIVEN_NAME_DESC = "Map `given_name` to the attribute for given name in the provider assertion or token."; - private static final String ALIAS_ID_DESC = "The ID of the mirrored IdP"; - private static final String ALIAS_ZID_DESC = "The ID of the identity zone to which this IdP should be mirrored"; + private static final String ALIAS_ID_DESC = "The ID of the alias IdP"; + private static final String ALIAS_ZID_DESC = "The ID of the identity zone in which an alias of this IdP should be maintained"; private static final FieldDescriptor STORE_CUSTOM_ATTRIBUTES = fieldWithPath("config.storeCustomAttributes").optional(true).type(BOOLEAN).description("Set to true, to store custom user attributes to be fetched from the /userinfo endpoint"); private static final FieldDescriptor SKIP_SSL_VALIDATION = fieldWithPath("config.skipSslValidation").optional(false).type(BOOLEAN).description("Set to true, to skip SSL validation when fetching metadata."); From 15b4c0f934eacdc91206fa0154f7893390680e58 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 12 Jan 2024 13:51:39 +0100 Subject: [PATCH 62/91] Improve endpoint documentation for identity provider alias properties --- .../uaa/provider/IdentityProvider.java | 6 +- .../provider/IdentityProviderEndpoints.java | 2 +- .../IdentityProviderEndpointDocs.java | 119 ++++++++++++------ 3 files changed, 87 insertions(+), 40 deletions(-) diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProvider.java b/model/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProvider.java index 667c333c980..4718d978ad6 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProvider.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/provider/IdentityProvider.java @@ -56,8 +56,8 @@ public class IdentityProvider { public static final String FIELD_IDENTITY_ZONE_ID = "identityZoneId"; public static final String FIELD_CONFIG = "config"; public static final String FIELD_TYPE = "type"; - public static final String FIELD_ALIAS_ID = "alias_id"; - public static final String FIELD_ALIAS_ZID = "alias_zid"; + public static final String FIELD_ALIAS_ID = "aliasId"; + public static final String FIELD_ALIAS_ZID = "aliasZid"; //see deserializer at the bottom private String id; @@ -74,9 +74,7 @@ public class IdentityProvider { private Date lastModified = new Date(); private boolean active = true; private String identityZoneId; - @JsonProperty("alias_id") private String aliasId; - @JsonProperty("alias_zid") private String aliasZid; public Date getCreated() { return created; 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 e5e1d2ad11a..0e551816d6d 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 @@ -83,7 +83,7 @@ public class IdentityProviderEndpoints implements ApplicationEventPublisherAware protected static Logger logger = LoggerFactory.getLogger(IdentityProviderEndpoints.class); /** - * The IdP types for which alias IdPs (via 'alias_id' and 'alias_zid') are supported. + * The IdP types for which alias IdPs (via 'aliasId' and 'aliasZid') are supported. */ private static final Set IDP_TYPES_ALIAS_SUPPORTED = Set.of(SAML, OAUTH20, OIDC10); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java index 385cb899fe8..c90fbdc9ce7 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java @@ -12,6 +12,60 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.mock.providers; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LDAP; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OIDC10; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.SAML; +import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; +import static org.cloudfoundry.identity.uaa.provider.IdentityProvider.FIELD_ALIAS_ID; +import static org.cloudfoundry.identity.uaa.provider.IdentityProvider.FIELD_ALIAS_ZID; +import static org.cloudfoundry.identity.uaa.provider.IdentityProvider.FIELD_IDENTITY_ZONE_ID; +import static org.cloudfoundry.identity.uaa.provider.IdentityProvider.FIELD_TYPE; +import static org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition.MAIL; +import static org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition.EMAIL_ATTRIBUTE_NAME; +import static org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition.EMAIL_VERIFIED_ATTRIBUTE_NAME; +import static org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition.ExternalGroupMappingMode; +import static org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition.FAMILY_NAME_ATTRIBUTE_NAME; +import static org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition.GIVEN_NAME_ATTRIBUTE_NAME; +import static org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition.GROUP_ATTRIBUTE_NAME; +import static org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition.PHONE_NUMBER_ATTRIBUTE_NAME; +import static org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition.USER_ATTRIBUTE_PREFIX; +import static org.cloudfoundry.identity.uaa.test.SnippetUtils.fieldWithPath; +import static org.cloudfoundry.identity.uaa.test.SnippetUtils.parameterWithName; +import static org.cloudfoundry.identity.uaa.util.JsonUtils.serializeExcludingProperties; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; +import static org.springframework.restdocs.payload.JsonFieldType.BOOLEAN; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.OBJECT; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.apache.commons.collections.map.HashedMap; import org.apache.commons.lang.ArrayUtils; import org.cloudfoundry.identity.uaa.constants.OriginKeys; @@ -21,7 +75,18 @@ import org.cloudfoundry.identity.uaa.mock.EndpointDocs; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.IdentityZoneCreationResult; -import org.cloudfoundry.identity.uaa.provider.*; +import org.cloudfoundry.identity.uaa.provider.AbstractExternalOAuthIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.IdentityProvider; +import org.cloudfoundry.identity.uaa.provider.IdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.provider.IdentityProviderStatus; +import org.cloudfoundry.identity.uaa.provider.JdbcIdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.LockoutPolicy; +import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.PasswordPolicy; +import org.cloudfoundry.identity.uaa.provider.RawExternalOAuthIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition; +import org.cloudfoundry.identity.uaa.provider.UaaIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.ldap.DynamicPasswordComparator; import org.cloudfoundry.identity.uaa.provider.saml.BootstrapSamlIdentityProviderDataTests; import org.cloudfoundry.identity.uaa.test.InMemoryLdapServer; @@ -31,7 +96,11 @@ import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder; import org.cloudfoundry.identity.uaa.zone.IdentityZoneSwitchingFilter; import org.cloudfoundry.identity.uaa.zone.MultitenancyFixture; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.restdocs.headers.HeaderDescriptor; import org.springframework.restdocs.payload.FieldDescriptor; @@ -40,32 +109,6 @@ import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.test.web.servlet.ResultActions; -import java.net.URL; -import java.util.*; - -import static org.cloudfoundry.identity.uaa.constants.OriginKeys.*; -import static org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils.CookieCsrfPostProcessor.cookieCsrf; -import static org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition.MAIL; -import static org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition.*; -import static org.cloudfoundry.identity.uaa.test.SnippetUtils.fieldWithPath; -import static org.cloudfoundry.identity.uaa.test.SnippetUtils.parameterWithName; -import static org.cloudfoundry.identity.uaa.util.JsonUtils.serializeExcludingProperties; -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; -import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; -import static org.springframework.restdocs.payload.JsonFieldType.*; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; -import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - class IdentityProviderEndpointDocs extends EndpointDocs { private static final String NAME_DESC = "Human-readable name for this provider"; @@ -79,8 +122,14 @@ class IdentityProviderEndpointDocs extends EndpointDocs { private static final String FAMILY_NAME_DESC = "Map `family_name` to the attribute for family name in the provider assertion or token."; private static final String PHONE_NUMBER_DESC = "Map `phone_number` to the attribute for phone number in the provider assertion or token."; private static final String GIVEN_NAME_DESC = "Map `given_name` to the attribute for given name in the provider assertion or token."; - private static final String ALIAS_ID_DESC = "The ID of the alias IdP"; - private static final String ALIAS_ZID_DESC = "The ID of the identity zone in which an alias of this IdP should be maintained"; + private static final String ALIAS_ID_DESC = "The ID of the alias IdP. This field may only be set for update operations on identity providers that already referenced an alias before. " + + "Otherwise, the field must be set to `null`."; + private static final String ALIAS_ZID_DESC = "The ID of the identity zone in which an alias of this IdP should be maintained. Defaults to `null`. " + + "If set and the identity provider did not reference an alias before, an alias identity provider is created in the referenced zone and `" + FIELD_ALIAS_ID + "` is set accordingly. " + + "Since alias identity providers are only supported for identity providers of type \"" + SAML + "\", \"" + OIDC10 + "\" and \"" + OAUTH20 + "\", this field must be set to `null` if `" + FIELD_TYPE + "` is not any of these values. " + + "Alias identity providers can only be created from or to the \"uaa\" identity zone, i.e., one of `" + FIELD_IDENTITY_ZONE_ID + "` or `" + FIELD_ALIAS_ZID + "` must be set to \"uaa\". " + + "If set, the field must reference an existing identity zone that is different to the one referenced in `" + FIELD_IDENTITY_ZONE_ID + "`. " + + "For updating an identity provider that already references an alias identity provider, this field must be left unchanged."; private static final FieldDescriptor STORE_CUSTOM_ATTRIBUTES = fieldWithPath("config.storeCustomAttributes").optional(true).type(BOOLEAN).description("Set to true, to store custom user attributes to be fetched from the /userinfo endpoint"); private static final FieldDescriptor SKIP_SSL_VALIDATION = fieldWithPath("config.skipSslValidation").optional(false).type(BOOLEAN).description("Set to true, to skip SSL validation when fetching metadata."); @@ -102,15 +151,15 @@ class IdentityProviderEndpointDocs extends EndpointDocs { private static final FieldDescriptor ID = fieldWithPath("id").type(STRING).description(ID_DESC); private static final FieldDescriptor CREATED = fieldWithPath("created").description(CREATED_DESC); private static final FieldDescriptor LAST_MODIFIED = fieldWithPath("last_modified").description(LAST_MODIFIED_DESC); - private static final FieldDescriptor ALIAS_ID = fieldWithPath("alias_id").description(ALIAS_ID_DESC).attributes(key("constraints").value("Optional")).optional().type(STRING); - private static final FieldDescriptor ALIAS_ZID = fieldWithPath("alias_zid").description(ALIAS_ZID_DESC).attributes(key("constraints").value("Optional")).optional().type(STRING); + private static final FieldDescriptor ALIAS_ID = fieldWithPath(FIELD_ALIAS_ID).description(ALIAS_ID_DESC).attributes(key("constraints").value("Optional")).optional().type(STRING); + private static final FieldDescriptor ALIAS_ZID = fieldWithPath(FIELD_ALIAS_ZID).description(ALIAS_ZID_DESC).attributes(key("constraints").value("Optional")).optional().type(STRING); private static final FieldDescriptor GROUP_WHITELIST = fieldWithPath("config.externalGroupsWhitelist").optional(null).type(ARRAY).description("JSON Array containing the groups names which need to be populated in the user's `id_token` or response from `/userinfo` endpoint. If you don't specify the whitelist no groups will be populated in the `id_token` or `/userinfo` response." + "
        Please note that regex is allowed. Acceptable patterns are" + "
        • `*` translates to all groups
        • " + "
        • `*pattern*` Contains pattern
        • " + "
        • `pattern*` Starts with pattern
        • " + "
        • `*pattern` Ends with pattern
        "); - private static final FieldDescriptor IDENTITY_ZONE_ID = fieldWithPath("identityZoneId").type(STRING).description(IDENTITY_ZONE_ID_DESC); + private static final FieldDescriptor IDENTITY_ZONE_ID = fieldWithPath(FIELD_IDENTITY_ZONE_ID).type(STRING).description(IDENTITY_ZONE_ID_DESC); private static final FieldDescriptor ADDITIONAL_CONFIGURATION = fieldWithPath("config.additionalConfiguration").optional(null).type(OBJECT).description("(Unused.)"); private static final SnippetUtils.ConstrainableField VERSION = (SnippetUtils.ConstrainableField) fieldWithPath("version").type(NUMBER).description(VERSION_DESC); private static final Snippet commonRequestParams = requestParameters(parameterWithName("rawConfig").optional("false").type(BOOLEAN).description("UAA 3.4.0 Flag indicating whether the response should use raw, unescaped JSON for the `config` field of the IDP, rather than the default behavior of encoding the JSON as a string.")); @@ -775,8 +824,8 @@ void getAllIdentityProviders() throws Exception { fieldWithPath("[].originKey").description("Unique identifier for the identity provider."), fieldWithPath("[].name").description(NAME_DESC), fieldWithPath("[].config").description(CONFIG_DESCRIPTION), - fieldWithPath("[].alias_id").description(ALIAS_ID_DESC), - fieldWithPath("[].alias_zid").description(ALIAS_ZID), + fieldWithPath("[]." + FIELD_ALIAS_ID).description(ALIAS_ID_DESC), + fieldWithPath("[]." + FIELD_ALIAS_ZID).description(ALIAS_ZID), fieldWithPath("[].version").description(VERSION_DESC), fieldWithPath("[].active").description(ACTIVE_DESC), From b556f632521aa4ba74401330e72d2a3b54f59cda Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 12 Jan 2024 16:49:01 +0100 Subject: [PATCH 63/91] Improve endpoint documentation --- .../IdentityProviderEndpointDocs.java | 558 ++++++++++-------- 1 file changed, 317 insertions(+), 241 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java index c90fbdc9ce7..a546a2f8296 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java @@ -12,6 +12,7 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.mock.providers; +import static java.util.function.Function.identity; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LDAP; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OIDC10; @@ -20,7 +21,6 @@ import static org.cloudfoundry.identity.uaa.provider.IdentityProvider.FIELD_ALIAS_ID; import static org.cloudfoundry.identity.uaa.provider.IdentityProvider.FIELD_ALIAS_ZID; import static org.cloudfoundry.identity.uaa.provider.IdentityProvider.FIELD_IDENTITY_ZONE_ID; -import static org.cloudfoundry.identity.uaa.provider.IdentityProvider.FIELD_TYPE; import static org.cloudfoundry.identity.uaa.provider.LdapIdentityProviderDefinition.MAIL; import static org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition.EMAIL_ATTRIBUTE_NAME; import static org.cloudfoundry.identity.uaa.provider.SamlIdentityProviderDefinition.EMAIL_VERIFIED_ATTRIBUTE_NAME; @@ -65,6 +65,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Stream; import org.apache.commons.collections.map.HashedMap; import org.apache.commons.lang.ArrayUtils; @@ -122,15 +123,14 @@ class IdentityProviderEndpointDocs extends EndpointDocs { private static final String FAMILY_NAME_DESC = "Map `family_name` to the attribute for family name in the provider assertion or token."; private static final String PHONE_NUMBER_DESC = "Map `phone_number` to the attribute for phone number in the provider assertion or token."; private static final String GIVEN_NAME_DESC = "Map `given_name` to the attribute for given name in the provider assertion or token."; - private static final String ALIAS_ID_DESC = "The ID of the alias IdP. This field may only be set for update operations on identity providers that already referenced an alias before. " + - "Otherwise, the field must be set to `null`."; - private static final String ALIAS_ZID_DESC = "The ID of the identity zone in which an alias of this IdP should be maintained. Defaults to `null`. " + - "If set and the identity provider did not reference an alias before, an alias identity provider is created in the referenced zone and `" + FIELD_ALIAS_ID + "` is set accordingly. " + - "Since alias identity providers are only supported for identity providers of type \"" + SAML + "\", \"" + OIDC10 + "\" and \"" + OAUTH20 + "\", this field must be set to `null` if `" + FIELD_TYPE + "` is not any of these values. " + - "Alias identity providers can only be created from or to the \"uaa\" identity zone, i.e., one of `" + FIELD_IDENTITY_ZONE_ID + "` or `" + FIELD_ALIAS_ZID + "` must be set to \"uaa\". " + + private static final String ALIAS_ID_DESC = "The ID of the alias IdP."; + private static final String ALIAS_ZID_DESC = "The ID of the identity zone in which an alias of this IdP is maintained."; + private static final String ALIAS_ZID_DESC_CREATE = ALIAS_ZID_DESC + + " Defaults to `null`. " + + "Only supported for identity providers of type \"" + SAML + "\", \"" + OIDC10 + "\" and \"" + OAUTH20 + "\". " + "If set, the field must reference an existing identity zone that is different to the one referenced in `" + FIELD_IDENTITY_ZONE_ID + "`. " + - "For updating an identity provider that already references an alias identity provider, this field must be left unchanged."; - + "Alias identity providers can only be created from or to the \"uaa\" identity zone, i.e., one of `" + FIELD_IDENTITY_ZONE_ID + "` or `" + FIELD_ALIAS_ZID + "` must be set to \"uaa\". " + + "If set and the identity provider did not reference an alias before, an alias identity provider is created in the referenced zone and `" + FIELD_ALIAS_ID + "` is set accordingly. "; private static final FieldDescriptor STORE_CUSTOM_ATTRIBUTES = fieldWithPath("config.storeCustomAttributes").optional(true).type(BOOLEAN).description("Set to true, to store custom user attributes to be fetched from the /userinfo endpoint"); private static final FieldDescriptor SKIP_SSL_VALIDATION = fieldWithPath("config.skipSslValidation").optional(false).type(BOOLEAN).description("Set to true, to skip SSL validation when fetching metadata."); private static final FieldDescriptor ATTRIBUTE_MAPPING = fieldWithPath("config.attributeMappings").optional(null).type(OBJECT).description("Map external attribute to UAA recognized mappings."); @@ -151,8 +151,8 @@ class IdentityProviderEndpointDocs extends EndpointDocs { private static final FieldDescriptor ID = fieldWithPath("id").type(STRING).description(ID_DESC); private static final FieldDescriptor CREATED = fieldWithPath("created").description(CREATED_DESC); private static final FieldDescriptor LAST_MODIFIED = fieldWithPath("last_modified").description(LAST_MODIFIED_DESC); - private static final FieldDescriptor ALIAS_ID = fieldWithPath(FIELD_ALIAS_ID).description(ALIAS_ID_DESC).attributes(key("constraints").value("Optional")).optional().type(STRING); - private static final FieldDescriptor ALIAS_ZID = fieldWithPath(FIELD_ALIAS_ZID).description(ALIAS_ZID_DESC).attributes(key("constraints").value("Optional")).optional().type(STRING); + private static final FieldDescriptor ALIAS_ID = fieldWithPath(FIELD_ALIAS_ID).attributes(key("constraints").value("Optional")).optional().type(STRING); + private static final FieldDescriptor ALIAS_ZID = fieldWithPath(FIELD_ALIAS_ZID).attributes(key("constraints").value("Optional")).optional().type(STRING); private static final FieldDescriptor GROUP_WHITELIST = fieldWithPath("config.externalGroupsWhitelist").optional(null).type(ARRAY).description("JSON Array containing the groups names which need to be populated in the user's `id_token` or response from `/userinfo` endpoint. If you don't specify the whitelist no groups will be populated in the `id_token` or `/userinfo` response." + "
        Please note that regex is allowed. Acceptable patterns are" + "
        • `*` translates to all groups
        • " + @@ -175,9 +175,27 @@ class IdentityProviderEndpointDocs extends EndpointDocs { EMAIL_DOMAIN, ACTIVE, ADD_SHADOW_USER, - STORE_CUSTOM_ATTRIBUTES, - ALIAS_ID, - ALIAS_ZID + STORE_CUSTOM_ATTRIBUTES + }; + + private static final FieldDescriptor[] ALIAS_FIELDS_GET = { + ALIAS_ID.description(ALIAS_ID_DESC), + ALIAS_ZID.description(ALIAS_ZID_DESC) + }; + + private static final FieldDescriptor[] ALIAS_FIELDS_CREATE = { + ALIAS_ID.description(ALIAS_ID_DESC + " Must be set to `null`."), + ALIAS_ZID.description(ALIAS_ZID_DESC_CREATE) + }; + + private static final FieldDescriptor[] ALIAS_FIELDS_LDAP_CREATE = { + ALIAS_ID.description(ALIAS_ID_DESC + " Must be set to `null`, since alias identity providers are not supported for LDAP."), + ALIAS_ZID.description(ALIAS_ZID_DESC + " Must be set to `null`, since alias identity providers are not supported for LDAP.") + }; + + private static final FieldDescriptor[] ALIAS_FIELDS_UPDATE = { + ALIAS_ID.description(ALIAS_ID_DESC + " The `" + FIELD_ALIAS_ID + "` value of the existing identity provider must be left unchanged."), + ALIAS_ZID.description(ALIAS_ZID_DESC_CREATE + " If the identity provider already referenced an alias identity provider before the update, this field must be left unchanged.") }; private FieldDescriptor[] attributeMappingFields = { @@ -284,116 +302,134 @@ static void startLdapContainer() { }); - private FieldDescriptor[] ldap_SearchAndCompare_GroupsAsScopes = (FieldDescriptor[]) ArrayUtils.addAll(commonProviderFields, new FieldDescriptor[]{ - LDAP_TYPE, - LDAP_ORIGIN_KEY, - LDAP_PROFILE_FILE, - LDAP_GROUP_FILE, - LDAP_URL, - LDAP_BIND_USER_DN, - LDAP_BIND_PASSWORD, - LDAP_USER_SEARCH_BASE, - LDAP_USER_SEARCH_FILTER, - LDAP_GROUP_SEARCH_BASE, - LDAP_GROUP_SEARCH_FILTER, - LDAP_GROUP_AUTO_ADD, - LDAP_GROUP_SEARCH_SUBTREE, - LDAP_GROUP_MAX_SEARCH_DEPTH, - LDAP_USER_MAIL_ATTRIBUTE, - LDAP_USER_MAIL_SUBSTITUTE, - LDAP_USER_MAIL_SUBSTITUTE_OVERRIDES_LDAP, - LDAP_SSL_SKIP_VERIFICATION, - LDAP_SSL_TLS, - LDAP_REFERRAL, - LDAP_GROUPS_IGNORE_PARTIAL, - LDAP_USER_DN_PATTERN.ignored(), - LDAP_USER_DN_PATTERN_DELIM.ignored(), - LDAP_USER_COMPARE_PASSWORD_ATTRIBUTE_NAME, - LDAP_USER_COMPARE_ENCODER, - LDAP_USER_COMPARE_LOCAL, - LDAP_GROUP_ROLE_ATTRIBUTE, - ATTRIBUTE_MAPPING, - LDAP_ATTRIBUTE_MAPPING_USER_NAME, - LDAP_ATTRIBUTE_MAPPING_FIRSTNAME, - LDAP_ATTRIBUTE_MAPPING_LASTNAME, - LDAP_ATTRIBUTE_MAPPING_PHONE, - ATTRIBUTE_MAPPING_EMAIL_VERIFIED_FIELD, - EXTERNAL_GROUPS_WHITELIST - }); - - private FieldDescriptor[] ldapSimpleBindFields = (FieldDescriptor[]) ArrayUtils.addAll(commonProviderFields, new FieldDescriptor[]{ - LDAP_TYPE, - LDAP_ORIGIN_KEY, - LDAP_PROFILE_FILE, - LDAP_GROUP_FILE, - LDAP_URL, - LDAP_USER_MAIL_ATTRIBUTE, - LDAP_USER_MAIL_SUBSTITUTE, - LDAP_USER_MAIL_SUBSTITUTE_OVERRIDES_LDAP, - LDAP_SSL_SKIP_VERIFICATION, - LDAP_SSL_TLS, - LDAP_REFERRAL, - LDAP_USER_DN_PATTERN, - LDAP_USER_DN_PATTERN_DELIM, - ATTRIBUTE_MAPPING, - LDAP_ATTRIBUTE_MAPPING_USER_NAME, - LDAP_ATTRIBUTE_MAPPING_FIRSTNAME, - LDAP_ATTRIBUTE_MAPPING_LASTNAME, - LDAP_ATTRIBUTE_MAPPING_PHONE, - ATTRIBUTE_MAPPING_EMAIL_VERIFIED_FIELD, - LDAP_BIND_USER_DN.ignored(), - LDAP_USER_SEARCH_BASE.ignored(), - LDAP_USER_SEARCH_FILTER.ignored(), - LDAP_GROUP_SEARCH_BASE.ignored(), - LDAP_GROUP_SEARCH_FILTER.ignored(), - LDAP_GROUP_AUTO_ADD.ignored(), - LDAP_GROUP_SEARCH_SUBTREE.ignored(), - LDAP_GROUP_MAX_SEARCH_DEPTH.ignored(), - LDAP_GROUPS_IGNORE_PARTIAL.ignored(), - LDAP_USER_COMPARE_PASSWORD_ATTRIBUTE_NAME.ignored(), - LDAP_USER_COMPARE_ENCODER.ignored(), - LDAP_USER_COMPARE_LOCAL.ignored(), - LDAP_GROUP_ROLE_ATTRIBUTE.ignored(), - EXTERNAL_GROUPS_WHITELIST.ignored() - }); - - - private FieldDescriptor[] ldapSearchAndBind_GroupsToScopes = (FieldDescriptor[]) ArrayUtils.addAll(commonProviderFields, new FieldDescriptor[]{ - LDAP_TYPE, - LDAP_ORIGIN_KEY, - LDAP_PROFILE_FILE, - LDAP_GROUP_FILE, - LDAP_URL, - LDAP_BIND_USER_DN, - LDAP_BIND_PASSWORD, - LDAP_USER_SEARCH_BASE, - LDAP_USER_SEARCH_FILTER, - LDAP_GROUP_SEARCH_BASE, - LDAP_GROUP_SEARCH_FILTER, - LDAP_GROUP_AUTO_ADD.ignored(), - LDAP_GROUP_SEARCH_SUBTREE, - LDAP_GROUP_MAX_SEARCH_DEPTH, - LDAP_USER_MAIL_ATTRIBUTE, - LDAP_USER_MAIL_SUBSTITUTE, - LDAP_USER_MAIL_SUBSTITUTE_OVERRIDES_LDAP, - LDAP_SSL_SKIP_VERIFICATION, - LDAP_SSL_TLS, - LDAP_REFERRAL, - LDAP_GROUPS_IGNORE_PARTIAL, - LDAP_USER_DN_PATTERN.ignored(), - LDAP_USER_DN_PATTERN_DELIM.ignored(), - LDAP_USER_COMPARE_PASSWORD_ATTRIBUTE_NAME.ignored(), - LDAP_USER_COMPARE_ENCODER.ignored(), - LDAP_USER_COMPARE_LOCAL.ignored(), - LDAP_GROUP_ROLE_ATTRIBUTE.ignored(), - ATTRIBUTE_MAPPING, - LDAP_ATTRIBUTE_MAPPING_USER_NAME, - LDAP_ATTRIBUTE_MAPPING_FIRSTNAME, - LDAP_ATTRIBUTE_MAPPING_LASTNAME, - LDAP_ATTRIBUTE_MAPPING_PHONE, - ATTRIBUTE_MAPPING_EMAIL_VERIFIED_FIELD, - EXTERNAL_GROUPS_WHITELIST - }); + private FieldDescriptor[] ldap_SearchAndCompare_GroupsAsScopes = (FieldDescriptor[]) ArrayUtils.addAll( + commonProviderFields, + ArrayUtils.addAll( + new FieldDescriptor[]{ + LDAP_TYPE, + LDAP_ORIGIN_KEY, + LDAP_PROFILE_FILE, + LDAP_GROUP_FILE, + LDAP_URL, + LDAP_BIND_USER_DN, + LDAP_BIND_PASSWORD, + LDAP_USER_SEARCH_BASE, + LDAP_USER_SEARCH_FILTER, + LDAP_GROUP_SEARCH_BASE, + LDAP_GROUP_SEARCH_FILTER, + LDAP_GROUP_AUTO_ADD, + LDAP_GROUP_SEARCH_SUBTREE, + LDAP_GROUP_MAX_SEARCH_DEPTH, + LDAP_USER_MAIL_ATTRIBUTE, + LDAP_USER_MAIL_SUBSTITUTE, + LDAP_USER_MAIL_SUBSTITUTE_OVERRIDES_LDAP, + LDAP_SSL_SKIP_VERIFICATION, + LDAP_SSL_TLS, + LDAP_REFERRAL, + LDAP_GROUPS_IGNORE_PARTIAL, + LDAP_USER_DN_PATTERN.ignored(), + LDAP_USER_DN_PATTERN_DELIM.ignored(), + LDAP_USER_COMPARE_PASSWORD_ATTRIBUTE_NAME, + LDAP_USER_COMPARE_ENCODER, + LDAP_USER_COMPARE_LOCAL, + LDAP_GROUP_ROLE_ATTRIBUTE, + ATTRIBUTE_MAPPING, + LDAP_ATTRIBUTE_MAPPING_USER_NAME, + LDAP_ATTRIBUTE_MAPPING_FIRSTNAME, + LDAP_ATTRIBUTE_MAPPING_LASTNAME, + LDAP_ATTRIBUTE_MAPPING_PHONE, + ATTRIBUTE_MAPPING_EMAIL_VERIFIED_FIELD, + EXTERNAL_GROUPS_WHITELIST + }, + ALIAS_FIELDS_LDAP_CREATE + ) + ); + + private FieldDescriptor[] ldapSimpleBindFields = (FieldDescriptor[]) ArrayUtils.addAll( + commonProviderFields, + ArrayUtils.addAll( + new FieldDescriptor[]{ + LDAP_TYPE, + LDAP_ORIGIN_KEY, + LDAP_PROFILE_FILE, + LDAP_GROUP_FILE, + LDAP_URL, + LDAP_USER_MAIL_ATTRIBUTE, + LDAP_USER_MAIL_SUBSTITUTE, + LDAP_USER_MAIL_SUBSTITUTE_OVERRIDES_LDAP, + LDAP_SSL_SKIP_VERIFICATION, + LDAP_SSL_TLS, + LDAP_REFERRAL, + LDAP_USER_DN_PATTERN, + LDAP_USER_DN_PATTERN_DELIM, + ATTRIBUTE_MAPPING, + LDAP_ATTRIBUTE_MAPPING_USER_NAME, + LDAP_ATTRIBUTE_MAPPING_FIRSTNAME, + LDAP_ATTRIBUTE_MAPPING_LASTNAME, + LDAP_ATTRIBUTE_MAPPING_PHONE, + ATTRIBUTE_MAPPING_EMAIL_VERIFIED_FIELD, + LDAP_BIND_USER_DN.ignored(), + LDAP_USER_SEARCH_BASE.ignored(), + LDAP_USER_SEARCH_FILTER.ignored(), + LDAP_GROUP_SEARCH_BASE.ignored(), + LDAP_GROUP_SEARCH_FILTER.ignored(), + LDAP_GROUP_AUTO_ADD.ignored(), + LDAP_GROUP_SEARCH_SUBTREE.ignored(), + LDAP_GROUP_MAX_SEARCH_DEPTH.ignored(), + LDAP_GROUPS_IGNORE_PARTIAL.ignored(), + LDAP_USER_COMPARE_PASSWORD_ATTRIBUTE_NAME.ignored(), + LDAP_USER_COMPARE_ENCODER.ignored(), + LDAP_USER_COMPARE_LOCAL.ignored(), + LDAP_GROUP_ROLE_ATTRIBUTE.ignored(), + EXTERNAL_GROUPS_WHITELIST.ignored() + }, + ALIAS_FIELDS_LDAP_CREATE + ) + ); + + + private FieldDescriptor[] ldapSearchAndBind_GroupsToScopes = (FieldDescriptor[]) ArrayUtils.addAll( + commonProviderFields, + ArrayUtils.addAll( + new FieldDescriptor[]{ + LDAP_TYPE, + LDAP_ORIGIN_KEY, + LDAP_PROFILE_FILE, + LDAP_GROUP_FILE, + LDAP_URL, + LDAP_BIND_USER_DN, + LDAP_BIND_PASSWORD, + LDAP_USER_SEARCH_BASE, + LDAP_USER_SEARCH_FILTER, + LDAP_GROUP_SEARCH_BASE, + LDAP_GROUP_SEARCH_FILTER, + LDAP_GROUP_AUTO_ADD.ignored(), + LDAP_GROUP_SEARCH_SUBTREE, + LDAP_GROUP_MAX_SEARCH_DEPTH, + LDAP_USER_MAIL_ATTRIBUTE, + LDAP_USER_MAIL_SUBSTITUTE, + LDAP_USER_MAIL_SUBSTITUTE_OVERRIDES_LDAP, + LDAP_SSL_SKIP_VERIFICATION, + LDAP_SSL_TLS, + LDAP_REFERRAL, + LDAP_GROUPS_IGNORE_PARTIAL, + LDAP_USER_DN_PATTERN.ignored(), + LDAP_USER_DN_PATTERN_DELIM.ignored(), + LDAP_USER_COMPARE_PASSWORD_ATTRIBUTE_NAME.ignored(), + LDAP_USER_COMPARE_ENCODER.ignored(), + LDAP_USER_COMPARE_LOCAL.ignored(), + LDAP_GROUP_ROLE_ATTRIBUTE.ignored(), + ATTRIBUTE_MAPPING, + LDAP_ATTRIBUTE_MAPPING_USER_NAME, + LDAP_ATTRIBUTE_MAPPING_FIRSTNAME, + LDAP_ATTRIBUTE_MAPPING_LASTNAME, + LDAP_ATTRIBUTE_MAPPING_PHONE, + ATTRIBUTE_MAPPING_EMAIL_VERIFIED_FIELD, + EXTERNAL_GROUPS_WHITELIST + }, + ALIAS_FIELDS_LDAP_CREATE + ) + ); @BeforeEach void setUp() throws Exception { @@ -415,24 +451,30 @@ void createSAMLIdentityProvider() throws Exception { IdentityProvider identityProvider = getSamlProvider("SAML"); identityProvider.setSerializeConfigRaw(true); - FieldDescriptor[] idempotentFields = (FieldDescriptor[]) ArrayUtils.addAll(commonProviderFields, ArrayUtils.addAll(new FieldDescriptor[]{ - fieldWithPath("type").required().description("`saml`"), - fieldWithPath("originKey").required().description("A unique alias for the SAML provider"), - SKIP_SSL_VALIDATION, - STORE_CUSTOM_ATTRIBUTES, - fieldWithPath("config.metaDataLocation").required().type(STRING).description("SAML Metadata - either an XML string or a URL that will deliver XML content"), - fieldWithPath("config.nameID").optional(null).type(STRING).description("The name ID to use for the username, default is \"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\"."), - fieldWithPath("config.assertionConsumerIndex").optional(null).type(NUMBER).description("SAML assertion consumer index, default is 0"), - fieldWithPath("config.metadataTrustCheck").optional(null).type(BOOLEAN).description("Should metadata be validated, defaults to false"), - fieldWithPath("config.showSamlLink").optional(null).type(BOOLEAN).description("Should the SAML login link be displayed on the login page, defaults to false"), - fieldWithPath("config.linkText").constrained("Required if the ``showSamlLink`` is set to true").type(STRING).description("The link text for the SAML IDP on the login page"), - fieldWithPath("config.groupMappingMode").optional(ExternalGroupMappingMode.EXPLICITLY_MAPPED).type(STRING).description("Either ``EXPLICITLY_MAPPED`` in order to map external groups to OAuth scopes using the group mappings, or ``AS_SCOPES`` to use SAML group names as scopes."), - fieldWithPath("config.iconUrl").optional(null).type(STRING).description("Reserved for future use"), - fieldWithPath("config.socketFactoryClassName").optional(null).description("Property is deprecated and value is ignored."), - fieldWithPath("config.authnContext").optional(null).type(ARRAY).description("List of AuthnContextClassRef to include in the SAMLRequest. If not specified no AuthnContext will be requested."), - EXTERNAL_GROUPS_WHITELIST, - fieldWithPath("config.attributeMappings.user_name").optional("NameID").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for SAML is `NameID`."), - }, attributeMappingFields)); + FieldDescriptor[] idempotentFields = Stream.of( + Stream.of(commonProviderFields), + Stream.of( + fieldWithPath("type").required().description("`saml`"), + fieldWithPath("originKey").required().description("A unique alias for the SAML provider"), + SKIP_SSL_VALIDATION, + STORE_CUSTOM_ATTRIBUTES, + fieldWithPath("config.metaDataLocation").required().type(STRING).description("SAML Metadata - either an XML string or a URL that will deliver XML content"), + fieldWithPath("config.nameID").optional(null).type(STRING).description("The name ID to use for the username, default is \"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\"."), + fieldWithPath("config.assertionConsumerIndex").optional(null).type(NUMBER).description("SAML assertion consumer index, default is 0"), + fieldWithPath("config.metadataTrustCheck").optional(null).type(BOOLEAN).description("Should metadata be validated, defaults to false"), + fieldWithPath("config.showSamlLink").optional(null).type(BOOLEAN).description("Should the SAML login link be displayed on the login page, defaults to false"), + fieldWithPath("config.linkText").constrained("Required if the ``showSamlLink`` is set to true").type(STRING).description("The link text for the SAML IDP on the login page"), + fieldWithPath("config.groupMappingMode").optional(ExternalGroupMappingMode.EXPLICITLY_MAPPED).type(STRING).description("Either ``EXPLICITLY_MAPPED`` in order to map external groups to OAuth scopes using the group mappings, or ``AS_SCOPES`` to use SAML group names as scopes."), + fieldWithPath("config.iconUrl").optional(null).type(STRING).description("Reserved for future use"), + fieldWithPath("config.socketFactoryClassName").optional(null).description("Property is deprecated and value is ignored."), + fieldWithPath("config.authnContext").optional(null).type(ARRAY).description("List of AuthnContextClassRef to include in the SAMLRequest. If not specified no AuthnContext will be requested."), + EXTERNAL_GROUPS_WHITELIST, + fieldWithPath("config.attributeMappings.user_name").optional("NameID").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for SAML is `NameID`.") + ), + Stream.of(ALIAS_FIELDS_CREATE), + Stream.of(attributeMappingFields) + ).flatMap(identity()) + .toArray(FieldDescriptor[]::new); Snippet requestFields = requestFields(idempotentFields); @@ -517,30 +559,37 @@ void createOAuthIdentityProvider() throws Exception { identityProvider.setConfig(definition); identityProvider.setSerializeConfigRaw(true); - FieldDescriptor[] idempotentFields = (FieldDescriptor[]) ArrayUtils.addAll(commonProviderFields, ArrayUtils.addAll(new FieldDescriptor[]{ - fieldWithPath("type").required().description("`\"" + OAUTH20 + "\"`"), - fieldWithPath("originKey").required().description("A unique alias for a OAuth provider"), - fieldWithPath("config.authUrl").required().type(STRING).description("The OAuth 2.0 authorization endpoint URL"), - fieldWithPath("config.tokenUrl").required().type(STRING).description("The OAuth 2.0 token endpoint URL"), - fieldWithPath("config.tokenKeyUrl").optional(null).type(STRING).description("The URL of the token key endpoint which renders a verification key for validating token signatures"), - fieldWithPath("config.tokenKey").optional(null).type(STRING).description("A verification key for validating token signatures, set to null if a `tokenKeyUrl` is provided."), - fieldWithPath("config.userInfoUrl").optional(null).type(STRING).description("A URL for fetching user info attributes when queried with the obtained token authorization."), - fieldWithPath("config.showLinkText").optional(true).type(BOOLEAN).description("A flag controlling whether a link to this provider's login will be shown on the UAA login page"), - fieldWithPath("config.linkText").optional(null).type(STRING).description("Text to use for the login link to the provider"), - fieldWithPath("config.relyingPartyId").required().type(STRING).description("The client ID which is registered with the external OAuth provider for use by the UAA"), - fieldWithPath("config.skipSslValidation").optional(null).type(BOOLEAN).description("A flag controlling whether SSL validation should be skipped when communicating with the external OAuth server"), - fieldWithPath("config.scopes").optional(null).type(ARRAY).description("What scopes to request on a call to the external OAuth provider"), - fieldWithPath("config.checkTokenUrl").optional(null).type(OBJECT).description("Reserved for future OAuth use."), - fieldWithPath("config.logoutUrl").optional(null).type(OBJECT).description("OAuth 2.0 logout endpoint."), - fieldWithPath("config.responseType").optional("code").type(STRING).description("Response type for the authorize request, will be sent to OAuth server, defaults to `code`"), - fieldWithPath("config.clientAuthInBody").optional(false).type(BOOLEAN).description("Sends the client credentials in the token retrieval call as body parameters instead of a Basic Authorization header."), - fieldWithPath("config.pkce").optional(true).type(BOOLEAN).description("A flag controlling whether PKCE (RFC 7636) is active in authorization code flow when requesting tokens from the external provider."), - fieldWithPath("config.performRpInitiatedLogout").optional(true).type(BOOLEAN).description("A flag controlling whether to log out of the external provider after a successful UAA logout per [OIDC RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)"), - fieldWithPath("config.issuer").optional(null).type(STRING).description("The OAuth 2.0 token issuer. This value is used to validate the issuer inside the token."), - fieldWithPath("config.userPropagationParameter").optional("username").type(STRING).description("Name of the request parameter that is used to pass a known username when redirecting to this identity provider from the account chooser"), - fieldWithPath("config.attributeMappings.user_name").optional("sub").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for OpenID Connect is `sub`"), - fieldWithPath("config.groupMappingMode").optional(AbstractExternalOAuthIdentityProviderDefinition.OAuthGroupMappingMode.EXPLICITLY_MAPPED).type(STRING).description("Either ``EXPLICITLY_MAPPED`` in order to map external claim values to OAuth scopes using the group mappings, or ``AS_SCOPES`` to use claim values names as scopes. You need to define also ``external_groups`` for the mapping in order to use this feature."), - }, attributeMappingFields)); + FieldDescriptor[] idempotentFields = Stream.of( + Stream.of(commonProviderFields), + Stream.of( + fieldWithPath("type").required().description("`\"" + OAUTH20 + "\"`"), + fieldWithPath("originKey").required().description("A unique alias for a OAuth provider"), + fieldWithPath("config.authUrl").required().type(STRING).description("The OAuth 2.0 authorization endpoint URL"), + fieldWithPath("config.tokenUrl").required().type(STRING).description("The OAuth 2.0 token endpoint URL"), + fieldWithPath("config.tokenKeyUrl").optional(null).type(STRING).description("The URL of the token key endpoint which renders a verification key for validating token signatures"), + fieldWithPath("config.tokenKey").optional(null).type(STRING).description("A verification key for validating token signatures, set to null if a `tokenKeyUrl` is provided."), + fieldWithPath("config.userInfoUrl").optional(null).type(STRING).description("A URL for fetching user info attributes when queried with the obtained token authorization."), + fieldWithPath("config.showLinkText").optional(true).type(BOOLEAN).description("A flag controlling whether a link to this provider's login will be shown on the UAA login page"), + fieldWithPath("config.linkText").optional(null).type(STRING).description("Text to use for the login link to the provider"), + fieldWithPath("config.relyingPartyId").required().type(STRING).description("The client ID which is registered with the external OAuth provider for use by the UAA"), + fieldWithPath("config.skipSslValidation").optional(null).type(BOOLEAN).description("A flag controlling whether SSL validation should be skipped when communicating with the external OAuth server"), + fieldWithPath("config.scopes").optional(null).type(ARRAY).description("What scopes to request on a call to the external OAuth provider"), + fieldWithPath("config.checkTokenUrl").optional(null).type(OBJECT).description("Reserved for future OAuth use."), + fieldWithPath("config.logoutUrl").optional(null).type(OBJECT).description("OAuth 2.0 logout endpoint."), + fieldWithPath("config.responseType").optional("code").type(STRING).description("Response type for the authorize request, will be sent to OAuth server, defaults to `code`"), + fieldWithPath("config.clientAuthInBody").optional(false).type(BOOLEAN).description("Sends the client credentials in the token retrieval call as body parameters instead of a Basic Authorization header."), + fieldWithPath("config.pkce").optional(true).type(BOOLEAN).description("A flag controlling whether PKCE (RFC 7636) is active in authorization code flow when requesting tokens from the external provider."), + fieldWithPath("config.performRpInitiatedLogout").optional(true).type(BOOLEAN).description("A flag controlling whether to log out of the external provider after a successful UAA logout per [OIDC RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)"), + fieldWithPath("config.issuer").optional(null).type(STRING).description("The OAuth 2.0 token issuer. This value is used to validate the issuer inside the token."), + fieldWithPath("config.userPropagationParameter").optional("username").type(STRING).description("Name of the request parameter that is used to pass a known username when redirecting to this identity provider from the account chooser"), + fieldWithPath("config.attributeMappings.user_name").optional("sub").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for OpenID Connect is `sub`"), + fieldWithPath("config.groupMappingMode").optional(AbstractExternalOAuthIdentityProviderDefinition.OAuthGroupMappingMode.EXPLICITLY_MAPPED).type(STRING).description("Either ``EXPLICITLY_MAPPED`` in order to map external claim values to OAuth scopes using the group mappings, or ``AS_SCOPES`` to use claim values names as scopes. You need to define also ``external_groups`` for the mapping in order to use this feature.") + ), + Stream.of(ALIAS_FIELDS_CREATE), + Stream.of(attributeMappingFields) + ).flatMap(identity()) + .toArray(FieldDescriptor[]::new); + Snippet requestFields = requestFields((FieldDescriptor[]) ArrayUtils.add(idempotentFields, relyingPartySecret)); Snippet responseFields = responseFields((FieldDescriptor[]) ArrayUtils.addAll(idempotentFields, new FieldDescriptor[]{ VERSION, @@ -597,44 +646,51 @@ void createOidcIdentityProvider() throws Exception { identityProvider.setConfig(definition); identityProvider.setSerializeConfigRaw(true); - FieldDescriptor[] idempotentFields = (FieldDescriptor[]) ArrayUtils.addAll(commonProviderFields, ArrayUtils.addAll(new FieldDescriptor[]{ - fieldWithPath("type").required().description("`\"" + OIDC10 + "\"`"), - fieldWithPath("originKey").required().description("A unique alias for the OIDC 1.0 provider"), - fieldWithPath("config.discoveryUrl").optional(null).type(STRING).description("The OpenID Connect Discovery URL, typically ends with /.well-known/openid-configurationmit "), - fieldWithPath("config.authUrl").optional().type(STRING).description("The OIDC 1.0 authorization endpoint URL. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL.").attributes(new Attributes.Attribute("constraints", "Required unless `discoveryUrl` is set.")), - fieldWithPath("config.tokenUrl").optional().type(STRING).description("The OIDC 1.0 token endpoint URL. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL.").attributes(new Attributes.Attribute("constraints", "Required unless `discoveryUrl` is set.")), - fieldWithPath("config.tokenKeyUrl").optional(null).type(STRING).description("The URL of the token key endpoint which renders a verification key for validating token signatures. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL.").attributes(new Attributes.Attribute("constraints", "Required unless `discoveryUrl` is set.")), - fieldWithPath("config.tokenKey").optional(null).type(STRING).description("A verification key for validating token signatures. We recommend not setting this as it will not allow for key rotation. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL.").attributes(new Attributes.Attribute("constraints", "Required unless `discoveryUrl` is set.")), - fieldWithPath("config.showLinkText").optional(true).type(BOOLEAN).description("A flag controlling whether a link to this provider's login will be shown on the UAA login page"), - fieldWithPath("config.linkText").optional(null).type(STRING).description("Text to use for the login link to the provider"), - fieldWithPath("config.relyingPartyId").required().type(STRING).description("The client ID which is registered with the external OAuth provider for use by the UAA"), - fieldWithPath("config.skipSslValidation").optional(null).type(BOOLEAN).description("A flag controlling whether SSL validation should be skipped when communicating with the external OAuth server"), - fieldWithPath("config.scopes").optional(null).type(ARRAY).description("What scopes to request on a call to the external OAuth/OpenID provider. For example, can provide " + - "`openid`, `roles`, or `profile` to request ID token, scopes populated in the ID token external groups attribute mappings, or the user profile information, respectively."), - fieldWithPath("config.checkTokenUrl").optional(null).type(OBJECT).description("Reserved for future OAuth/OIDC use."), - fieldWithPath("config.clientAuthInBody").optional(false).type(BOOLEAN).description("Only effective if relyingPartySecret is defined. Sends the client credentials in the token retrieval call as body parameters instead of a Basic Authorization header. It is recommended to set `jwtClientAuthentication:true` instead."), - fieldWithPath("config.pkce").optional(true).type(BOOLEAN).description("A flag controlling whether PKCE (RFC 7636) is active in authorization code flow when requesting tokens from the external provider."), - fieldWithPath("config.performRpInitiatedLogout").optional(true).type(BOOLEAN).description("A flag controlling whether to log out of the external provider after a successful UAA logout per [OIDC RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)"), - fieldWithPath("config.userInfoUrl").optional(null).type(OBJECT).description("Reserved for future OIDC use. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL."), - fieldWithPath("config.logoutUrl").optional(null).type(OBJECT).description("OIDC logout endpoint. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL."), - fieldWithPath("config.responseType").optional("code").type(STRING).description("Response type for the authorize request, defaults to `code`, but can be `code id_token` if the OIDC server can return an id_token as a query parameter in the redirect."), - fieldWithPath("config.issuer").optional(null).type(STRING).description("The OAuth 2.0 token issuer. This value is used to validate the issuer inside the token."), - fieldWithPath("config.userPropagationParameter").optional("username").type(STRING).description("Name of the request parameter that is used to pass a known username when redirecting to this identity provider from the account chooser"), - GROUP_WHITELIST, - fieldWithPath("config.passwordGrantEnabled").optional(false).type(BOOLEAN).description("Enable Resource Owner Password Grant flow for this identity provider."), - fieldWithPath("config.setForwardHeader").optional(false).type(BOOLEAN).description("Only effective if Password Grant enabled. Set X-Forward-For header in Password Grant request to this identity provider."), - fieldWithPath("config.jwtClientAuthentication").optional(null).type(OBJECT).description("UAA 76.5.0 Only effective if relyingPartySecret is not set or null. Creates private_key_jwt client authentication according to OIDC or OAuth2 (RFC 7523) standard. "+ - "
          Please note that you can precise the created JWT for client authentication, e.g. if your IdP follows OAuth2 standard according to RFC 7523. For standard OIDC compliance, set true without any further sub-parameters. The supported sub-parameters are " + - "
          • `kid` UAA 76.18.0 Custom key from your defined keys, defaults to `activeKeyId`
          • " + - "
          • `iss` Custom issuer, see RFC 7523, defaults to `relyingPartyId`
          • " + - "
          • `aud` Custom audience, see RFC 7523, defaults to `tokenUrl`
          "), - fieldWithPath("config.attributeMappings.user_name").optional("sub").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for OpenID Connect is `sub`."), - fieldWithPath("config.additionalAuthzParameters").optional(null).type(OBJECT).description("UAA 76.17.0Map of key-value pairs that are added as additional parameters for grant type `authorization_code`. For example, configure an entry with key `token_format` and value `jwt`."), - fieldWithPath("config.prompts[]").optional(null).type(ARRAY).description("List of fields that users are prompted on to the OIDC provider through the password grant flow. Defaults to username, password, and passcode. Any additional prompts beyond username, password, and passcode will be forwarded on to the OIDC provider."), - fieldWithPath("config.prompts[].name").optional(null).type(STRING).description("Name of field"), - fieldWithPath("config.prompts[].type").optional(null).type(STRING).description("What kind of field this is (e.g. text or password)"), - fieldWithPath("config.prompts[].text").optional(null).type(STRING).description("Actual text displayed on prompt for field") - }, attributeMappingFields)); + FieldDescriptor[] idempotentFields = Stream.of( + Stream.of(commonProviderFields), + Stream.of( + fieldWithPath("type").required().description("`\"" + OIDC10 + "\"`"), + fieldWithPath("originKey").required().description("A unique alias for the OIDC 1.0 provider"), + fieldWithPath("config.discoveryUrl").optional(null).type(STRING).description("The OpenID Connect Discovery URL, typically ends with /.well-known/openid-configurationmit "), + fieldWithPath("config.authUrl").optional().type(STRING).description("The OIDC 1.0 authorization endpoint URL. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL.").attributes(new Attributes.Attribute("constraints", "Required unless `discoveryUrl` is set.")), + fieldWithPath("config.tokenUrl").optional().type(STRING).description("The OIDC 1.0 token endpoint URL. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL.").attributes(new Attributes.Attribute("constraints", "Required unless `discoveryUrl` is set.")), + fieldWithPath("config.tokenKeyUrl").optional(null).type(STRING).description("The URL of the token key endpoint which renders a verification key for validating token signatures. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL.").attributes(new Attributes.Attribute("constraints", "Required unless `discoveryUrl` is set.")), + fieldWithPath("config.tokenKey").optional(null).type(STRING).description("A verification key for validating token signatures. We recommend not setting this as it will not allow for key rotation. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL.").attributes(new Attributes.Attribute("constraints", "Required unless `discoveryUrl` is set.")), + fieldWithPath("config.showLinkText").optional(true).type(BOOLEAN).description("A flag controlling whether a link to this provider's login will be shown on the UAA login page"), + fieldWithPath("config.linkText").optional(null).type(STRING).description("Text to use for the login link to the provider"), + fieldWithPath("config.relyingPartyId").required().type(STRING).description("The client ID which is registered with the external OAuth provider for use by the UAA"), + fieldWithPath("config.skipSslValidation").optional(null).type(BOOLEAN).description("A flag controlling whether SSL validation should be skipped when communicating with the external OAuth server"), + fieldWithPath("config.scopes").optional(null).type(ARRAY).description("What scopes to request on a call to the external OAuth/OpenID provider. For example, can provide " + + "`openid`, `roles`, or `profile` to request ID token, scopes populated in the ID token external groups attribute mappings, or the user profile information, respectively."), + fieldWithPath("config.checkTokenUrl").optional(null).type(OBJECT).description("Reserved for future OAuth/OIDC use."), + fieldWithPath("config.clientAuthInBody").optional(false).type(BOOLEAN).description("Only effective if relyingPartySecret is defined. Sends the client credentials in the token retrieval call as body parameters instead of a Basic Authorization header. It is recommended to set `jwtClientAuthentication:true` instead."), + fieldWithPath("config.pkce").optional(true).type(BOOLEAN).description("A flag controlling whether PKCE (RFC 7636) is active in authorization code flow when requesting tokens from the external provider."), + fieldWithPath("config.performRpInitiatedLogout").optional(true).type(BOOLEAN).description("A flag controlling whether to log out of the external provider after a successful UAA logout per [OIDC RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)"), + fieldWithPath("config.userInfoUrl").optional(null).type(OBJECT).description("Reserved for future OIDC use. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL."), + fieldWithPath("config.logoutUrl").optional(null).type(OBJECT).description("OIDC logout endpoint. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL."), + fieldWithPath("config.responseType").optional("code").type(STRING).description("Response type for the authorize request, defaults to `code`, but can be `code id_token` if the OIDC server can return an id_token as a query parameter in the redirect."), + fieldWithPath("config.issuer").optional(null).type(STRING).description("The OAuth 2.0 token issuer. This value is used to validate the issuer inside the token."), + fieldWithPath("config.userPropagationParameter").optional("username").type(STRING).description("Name of the request parameter that is used to pass a known username when redirecting to this identity provider from the account chooser"), + GROUP_WHITELIST, + fieldWithPath("config.passwordGrantEnabled").optional(false).type(BOOLEAN).description("Enable Resource Owner Password Grant flow for this identity provider."), + fieldWithPath("config.setForwardHeader").optional(false).type(BOOLEAN).description("Only effective if Password Grant enabled. Set X-Forward-For header in Password Grant request to this identity provider."), + fieldWithPath("config.jwtClientAuthentication").optional(null).type(OBJECT).description("UAA 76.5.0 Only effective if relyingPartySecret is not set or null. Creates private_key_jwt client authentication according to OIDC or OAuth2 (RFC 7523) standard. " + + "
          Please note that you can precise the created JWT for client authentication, e.g. if your IdP follows OAuth2 standard according to RFC 7523. For standard OIDC compliance, set true without any further sub-parameters. The supported sub-parameters are " + + "
          • `kid` UAA 76.18.0 Custom key from your defined keys, defaults to `activeKeyId`
          • " + + "
          • `iss` Custom issuer, see RFC 7523, defaults to `relyingPartyId`
          • " + + "
          • `aud` Custom audience, see RFC 7523, defaults to `tokenUrl`
          "), + fieldWithPath("config.attributeMappings.user_name").optional("sub").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for OpenID Connect is `sub`."), + fieldWithPath("config.additionalAuthzParameters").optional(null).type(OBJECT).description("UAA 76.17.0Map of key-value pairs that are added as additional parameters for grant type `authorization_code`. For example, configure an entry with key `token_format` and value `jwt`."), + fieldWithPath("config.prompts[]").optional(null).type(ARRAY).description("List of fields that users are prompted on to the OIDC provider through the password grant flow. Defaults to username, password, and passcode. Any additional prompts beyond username, password, and passcode will be forwarded on to the OIDC provider."), + fieldWithPath("config.prompts[].name").optional(null).type(STRING).description("Name of field"), + fieldWithPath("config.prompts[].type").optional(null).type(STRING).description("What kind of field this is (e.g. text or password)"), + fieldWithPath("config.prompts[].text").optional(null).type(STRING).description("Actual text displayed on prompt for field") + ), + Stream.of(ALIAS_FIELDS_CREATE), + Stream.of(attributeMappingFields) + ).flatMap(identity()) + .toArray(FieldDescriptor[]::new); + Snippet requestFields = requestFields((FieldDescriptor[]) ArrayUtils.add(idempotentFields, relyingPartySecret)); Snippet responseFields = responseFields((FieldDescriptor[]) ArrayUtils.addAll(idempotentFields, new FieldDescriptor[]{ VERSION, @@ -774,14 +830,22 @@ void createLDAPProvider(IdentityProvider identit Snippet requestFields = requestFields(fields); - Snippet responseFields = responseFields((FieldDescriptor[]) ArrayUtils.addAll(ldapAllFields, new FieldDescriptor[]{ - VERSION, - ID, - ADDITIONAL_CONFIGURATION, - IDENTITY_ZONE_ID, - CREATED, - LAST_MODIFIED - })); + Snippet responseFields = responseFields( + (FieldDescriptor[]) ArrayUtils.addAll( + ldapAllFields, + ArrayUtils.addAll( + new FieldDescriptor[]{ + VERSION, + ID, + ADDITIONAL_CONFIGURATION, + IDENTITY_ZONE_ID, + CREATED, + LAST_MODIFIED + }, + ALIAS_FIELDS_GET + ) + ) + ); ResultActions resultActions = mockMvc.perform(post("/identity-providers") .header(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER, zone.getIdentityZone().getSubdomain()) @@ -891,24 +955,30 @@ void updateIdentityProvider() throws Exception { identityProvider.setConfig(config); identityProvider.setSerializeConfigRaw(true); - FieldDescriptor[] idempotentFields = (FieldDescriptor[]) ArrayUtils.addAll(commonProviderFields, new FieldDescriptor[]{ - fieldWithPath("type").required().description("`uaa`"), - fieldWithPath("originKey").required().description("A unique identifier for the IDP. Cannot be updated."), - VERSION.required(), - fieldWithPath("config.passwordPolicy").ignored(), - fieldWithPath("config.passwordPolicy.minLength").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of characters required for password to be considered valid (defaults to 0).").optional(), - fieldWithPath("config.passwordPolicy.maxLength").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Maximum number of characters required for password to be considered valid (defaults to 255).").optional(), - fieldWithPath("config.passwordPolicy.requireUpperCaseCharacter").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of uppercase characters required for password to be considered valid (defaults to 0).").optional(), - fieldWithPath("config.passwordPolicy.requireLowerCaseCharacter").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of lowercase characters required for password to be considered valid (defaults to 0).").optional(), - fieldWithPath("config.passwordPolicy.requireDigit").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of digits required for password to be considered valid (defaults to 0).").optional(), - fieldWithPath("config.passwordPolicy.requireSpecialCharacter").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of special characters required for password to be considered valid (defaults to 0).").optional(), - fieldWithPath("config.passwordPolicy.expirePasswordInMonths").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Number of months after which current password expires (defaults to 0).").optional(), - fieldWithPath("config.passwordPolicy.passwordNewerThan").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("This timestamp value can be used to force change password for every user. If the user's passwordLastModified is older than this value, the password is expired (defaults to null)."), - fieldWithPath("config.lockoutPolicy.lockoutPeriodSeconds").constrained("Required when `LockoutPolicy` in the config is not null").type(NUMBER).description("Number of seconds in which lockoutAfterFailures failures must occur in order for account to be locked (defaults to 3600).").optional(), - fieldWithPath("config.lockoutPolicy.lockoutAfterFailures").constrained("Required when `LockoutPolicy` in the config is not null").type(NUMBER).description("Number of allowed failures before account is locked (defaults to 5).").optional(), - fieldWithPath("config.lockoutPolicy.countFailuresWithin").constrained("Required when `LockoutPolicy` in the config is not null").type(NUMBER).description("Number of seconds to lock out an account when lockoutAfterFailures failures is exceeded (defaults to 300).").optional(), - fieldWithPath("config.disableInternalUserManagement").optional(null).type(BOOLEAN).description("When set to true, user management is disabled for this provider, defaults to false").optional() - }); + FieldDescriptor[] idempotentFields = Stream.of( + Stream.of(commonProviderFields), + Stream.of( + fieldWithPath("type").required().description("`uaa`"), + fieldWithPath("originKey").required().description("A unique identifier for the IDP. Cannot be updated."), + VERSION.required(), + fieldWithPath("config.passwordPolicy").ignored(), + fieldWithPath("config.passwordPolicy.minLength").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of characters required for password to be considered valid (defaults to 0).").optional(), + fieldWithPath("config.passwordPolicy.maxLength").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Maximum number of characters required for password to be considered valid (defaults to 255).").optional(), + fieldWithPath("config.passwordPolicy.requireUpperCaseCharacter").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of uppercase characters required for password to be considered valid (defaults to 0).").optional(), + fieldWithPath("config.passwordPolicy.requireLowerCaseCharacter").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of lowercase characters required for password to be considered valid (defaults to 0).").optional(), + fieldWithPath("config.passwordPolicy.requireDigit").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of digits required for password to be considered valid (defaults to 0).").optional(), + fieldWithPath("config.passwordPolicy.requireSpecialCharacter").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of special characters required for password to be considered valid (defaults to 0).").optional(), + fieldWithPath("config.passwordPolicy.expirePasswordInMonths").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Number of months after which current password expires (defaults to 0).").optional(), + fieldWithPath("config.passwordPolicy.passwordNewerThan").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("This timestamp value can be used to force change password for every user. If the user's passwordLastModified is older than this value, the password is expired (defaults to null)."), + fieldWithPath("config.lockoutPolicy.lockoutPeriodSeconds").constrained("Required when `LockoutPolicy` in the config is not null").type(NUMBER).description("Number of seconds in which lockoutAfterFailures failures must occur in order for account to be locked (defaults to 3600).").optional(), + fieldWithPath("config.lockoutPolicy.lockoutAfterFailures").constrained("Required when `LockoutPolicy` in the config is not null").type(NUMBER).description("Number of allowed failures before account is locked (defaults to 5).").optional(), + fieldWithPath("config.lockoutPolicy.countFailuresWithin").constrained("Required when `LockoutPolicy` in the config is not null").type(NUMBER).description("Number of seconds to lock out an account when lockoutAfterFailures failures is exceeded (defaults to 300).").optional(), + fieldWithPath("config.disableInternalUserManagement").optional(null).type(BOOLEAN).description("When set to true, user management is disabled for this provider, defaults to false").optional() + ), + Stream.of(ALIAS_FIELDS_UPDATE) + ).flatMap(identity()) + .toArray(FieldDescriptor[]::new); + Snippet requestFields = requestFields(idempotentFields); Snippet responseFields = responseFields((FieldDescriptor[]) ArrayUtils.addAll(idempotentFields, new FieldDescriptor[]{ @@ -1011,17 +1081,23 @@ private ResultActions deleteIdentityProviderHelper(String id) throws Exception { } private FieldDescriptor[] getCommonProviderFieldsAnyType() { - return (FieldDescriptor[]) ArrayUtils.addAll(commonProviderFields, new FieldDescriptor[]{ - fieldWithPath("type").required().description("Type of the identity provider."), - fieldWithPath("originKey").required().description("Unique identifier for the identity provider."), - CONFIG, - ADDITIONAL_CONFIGURATION, - VERSION, - ID, - IDENTITY_ZONE_ID, - CREATED, - LAST_MODIFIED - }); + return (FieldDescriptor[]) ArrayUtils.addAll( + commonProviderFields, + ArrayUtils.addAll( + new FieldDescriptor[]{ + fieldWithPath("type").required().description("Type of the identity provider."), + fieldWithPath("originKey").required().description("Unique identifier for the identity provider."), + CONFIG, + ADDITIONAL_CONFIGURATION, + VERSION, + ID, + IDENTITY_ZONE_ID, + CREATED, + LAST_MODIFIED + }, + ALIAS_FIELDS_GET + ) + ); } From 1b6798f52270877cd12e3f3da788d3392a6c31bd Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 12 Jan 2024 17:05:04 +0100 Subject: [PATCH 64/91] Fix endpoint docs of alias properties --- .../IdentityProviderEndpointDocs.java | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java index a546a2f8296..8239786371a 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java @@ -151,8 +151,6 @@ class IdentityProviderEndpointDocs extends EndpointDocs { private static final FieldDescriptor ID = fieldWithPath("id").type(STRING).description(ID_DESC); private static final FieldDescriptor CREATED = fieldWithPath("created").description(CREATED_DESC); private static final FieldDescriptor LAST_MODIFIED = fieldWithPath("last_modified").description(LAST_MODIFIED_DESC); - private static final FieldDescriptor ALIAS_ID = fieldWithPath(FIELD_ALIAS_ID).attributes(key("constraints").value("Optional")).optional().type(STRING); - private static final FieldDescriptor ALIAS_ZID = fieldWithPath(FIELD_ALIAS_ZID).attributes(key("constraints").value("Optional")).optional().type(STRING); private static final FieldDescriptor GROUP_WHITELIST = fieldWithPath("config.externalGroupsWhitelist").optional(null).type(ARRAY).description("JSON Array containing the groups names which need to be populated in the user's `id_token` or response from `/userinfo` endpoint. If you don't specify the whitelist no groups will be populated in the `id_token` or `/userinfo` response." + "
          Please note that regex is allowed. Acceptable patterns are" + "
          • `*` translates to all groups
          • " + @@ -179,23 +177,47 @@ class IdentityProviderEndpointDocs extends EndpointDocs { }; private static final FieldDescriptor[] ALIAS_FIELDS_GET = { - ALIAS_ID.description(ALIAS_ID_DESC), - ALIAS_ZID.description(ALIAS_ZID_DESC) + fieldWithPath(FIELD_ALIAS_ID) + .attributes(key("constraints").value("Optional")) + .optional().type(STRING) + .description(ALIAS_ID_DESC), + fieldWithPath(FIELD_ALIAS_ZID) + .attributes(key("constraints").value("Optional")) + .optional().type(STRING) + .description(ALIAS_ZID_DESC) }; private static final FieldDescriptor[] ALIAS_FIELDS_CREATE = { - ALIAS_ID.description(ALIAS_ID_DESC + " Must be set to `null`."), - ALIAS_ZID.description(ALIAS_ZID_DESC_CREATE) + fieldWithPath(FIELD_ALIAS_ID) + .attributes(key("constraints").value("Optional")) + .optional().type(STRING) + .description(ALIAS_ID_DESC + " Must be set to `null`."), + fieldWithPath(FIELD_ALIAS_ZID) + .attributes(key("constraints").value("Optional")) + .optional().type(STRING) + .description(ALIAS_ZID_DESC_CREATE) }; private static final FieldDescriptor[] ALIAS_FIELDS_LDAP_CREATE = { - ALIAS_ID.description(ALIAS_ID_DESC + " Must be set to `null`, since alias identity providers are not supported for LDAP."), - ALIAS_ZID.description(ALIAS_ZID_DESC + " Must be set to `null`, since alias identity providers are not supported for LDAP.") + fieldWithPath(FIELD_ALIAS_ID) + .attributes(key("constraints").value("Optional")) + .optional().type(STRING) + .description(ALIAS_ID_DESC + " Must be set to `null`, since alias identity providers are not supported for LDAP."), + fieldWithPath(FIELD_ALIAS_ZID) + .attributes(key("constraints").value("Optional")) + .optional().type(STRING) + .description(ALIAS_ZID_DESC + " Must be set to `null`, since alias identity providers are not supported for LDAP.") }; private static final FieldDescriptor[] ALIAS_FIELDS_UPDATE = { - ALIAS_ID.description(ALIAS_ID_DESC + " The `" + FIELD_ALIAS_ID + "` value of the existing identity provider must be left unchanged."), - ALIAS_ZID.description(ALIAS_ZID_DESC_CREATE + " If the identity provider already referenced an alias identity provider before the update, this field must be left unchanged.") + fieldWithPath(FIELD_ALIAS_ID) + .attributes(key("constraints").value("Optional")) + .optional().type(STRING) + .description(ALIAS_ID_DESC + " The `" + FIELD_ALIAS_ID + "` value of the existing identity provider must be left unchanged."), + fieldWithPath(FIELD_ALIAS_ZID) + .attributes(key("constraints").value("Optional")) + .optional().type(STRING) + .description(ALIAS_ZID_DESC_CREATE + " If the identity provider already referenced an alias identity provider before the update, this field must be left unchanged.") }; private FieldDescriptor[] attributeMappingFields = { @@ -889,7 +911,7 @@ void getAllIdentityProviders() throws Exception { fieldWithPath("[].name").description(NAME_DESC), fieldWithPath("[].config").description(CONFIG_DESCRIPTION), fieldWithPath("[]." + FIELD_ALIAS_ID).description(ALIAS_ID_DESC), - fieldWithPath("[]." + FIELD_ALIAS_ZID).description(ALIAS_ZID), + fieldWithPath("[]." + FIELD_ALIAS_ZID).description(fieldWithPath(FIELD_ALIAS_ZID).attributes(key("constraints").value("Optional")).optional().type(STRING)), fieldWithPath("[].version").description(VERSION_DESC), fieldWithPath("[].active").description(ACTIVE_DESC), From 1e5cb69557af898a363f6f945fc97507ede7f612 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 12 Jan 2024 18:44:20 +0100 Subject: [PATCH 65/91] Adjust description of update and create operation --- .../uaa/mock/providers/IdentityProviderEndpointDocs.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java index 8239786371a..9c85857807f 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java @@ -129,8 +129,7 @@ class IdentityProviderEndpointDocs extends EndpointDocs { " Defaults to `null`. " + "Only supported for identity providers of type \"" + SAML + "\", \"" + OIDC10 + "\" and \"" + OAUTH20 + "\". " + "If set, the field must reference an existing identity zone that is different to the one referenced in `" + FIELD_IDENTITY_ZONE_ID + "`. " + - "Alias identity providers can only be created from or to the \"uaa\" identity zone, i.e., one of `" + FIELD_IDENTITY_ZONE_ID + "` or `" + FIELD_ALIAS_ZID + "` must be set to \"uaa\". " + - "If set and the identity provider did not reference an alias before, an alias identity provider is created in the referenced zone and `" + FIELD_ALIAS_ID + "` is set accordingly. "; + "Alias identity providers can only be created from or to the \"uaa\" identity zone, i.e., one of `" + FIELD_IDENTITY_ZONE_ID + "` or `" + FIELD_ALIAS_ZID + "` must be set to \"uaa\"."; private static final FieldDescriptor STORE_CUSTOM_ATTRIBUTES = fieldWithPath("config.storeCustomAttributes").optional(true).type(BOOLEAN).description("Set to true, to store custom user attributes to be fetched from the /userinfo endpoint"); private static final FieldDescriptor SKIP_SSL_VALIDATION = fieldWithPath("config.skipSslValidation").optional(false).type(BOOLEAN).description("Set to true, to skip SSL validation when fetching metadata."); private static final FieldDescriptor ATTRIBUTE_MAPPING = fieldWithPath("config.attributeMappings").optional(null).type(OBJECT).description("Map external attribute to UAA recognized mappings."); @@ -195,7 +194,7 @@ class IdentityProviderEndpointDocs extends EndpointDocs { fieldWithPath(FIELD_ALIAS_ZID) .attributes(key("constraints").value("Optional")) .optional().type(STRING) - .description(ALIAS_ZID_DESC_CREATE) + .description(ALIAS_ZID_DESC_CREATE + " If set, an alias identity provider is created in the referenced zone and `\" + FIELD_ALIAS_ID + \"` is set accordingly.") }; private static final FieldDescriptor[] ALIAS_FIELDS_LDAP_CREATE = { @@ -217,7 +216,8 @@ class IdentityProviderEndpointDocs extends EndpointDocs { fieldWithPath(FIELD_ALIAS_ZID) .attributes(key("constraints").value("Optional")) .optional().type(STRING) - .description(ALIAS_ZID_DESC_CREATE + " If the identity provider already referenced an alias identity provider before the update, this field must be left unchanged.") + .description(ALIAS_ZID_DESC_CREATE + " If set and the identity provider did not reference an alias before, an alias identity provider is created in the referenced zone and `\" + FIELD_ALIAS_ID + \"` is set accordingly. " + + "If the identity provider already referenced an alias identity provider before the update, this field must be left unchanged.") }; private FieldDescriptor[] attributeMappingFields = { From 3d9f9d91feb9983c7ca784a0cd11781c9a219dc3 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 12 Jan 2024 18:52:28 +0100 Subject: [PATCH 66/91] Fix endpoint docs again --- .../uaa/mock/providers/IdentityProviderEndpointDocs.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java index 9c85857807f..55bf49dd367 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java @@ -194,7 +194,7 @@ class IdentityProviderEndpointDocs extends EndpointDocs { fieldWithPath(FIELD_ALIAS_ZID) .attributes(key("constraints").value("Optional")) .optional().type(STRING) - .description(ALIAS_ZID_DESC_CREATE + " If set, an alias identity provider is created in the referenced zone and `\" + FIELD_ALIAS_ID + \"` is set accordingly.") + .description(ALIAS_ZID_DESC_CREATE + " If set, an alias identity provider is created in the referenced zone and `" + FIELD_ALIAS_ID + "` is set accordingly.") }; private static final FieldDescriptor[] ALIAS_FIELDS_LDAP_CREATE = { @@ -216,7 +216,7 @@ class IdentityProviderEndpointDocs extends EndpointDocs { fieldWithPath(FIELD_ALIAS_ZID) .attributes(key("constraints").value("Optional")) .optional().type(STRING) - .description(ALIAS_ZID_DESC_CREATE + " If set and the identity provider did not reference an alias before, an alias identity provider is created in the referenced zone and `\" + FIELD_ALIAS_ID + \"` is set accordingly. " + + .description(ALIAS_ZID_DESC_CREATE + " If set and the identity provider did not reference an alias before, an alias identity provider is created in the referenced zone and `" + FIELD_ALIAS_ID + "` is set accordingly. " + "If the identity provider already referenced an alias identity provider before the update, this field must be left unchanged.") }; From bd5e70cc533d430f6d7f9a5841e2e26985f60796 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Fri, 12 Jan 2024 19:41:43 +0100 Subject: [PATCH 67/91] Fix endpoint docs again --- .../IdentityProviderEndpointDocs.java | 147 ++++++++++++------ 1 file changed, 96 insertions(+), 51 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java index 55bf49dd367..c69f90823b8 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java @@ -65,6 +65,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.collections.map.HashedMap; @@ -493,23 +494,30 @@ void createSAMLIdentityProvider() throws Exception { EXTERNAL_GROUPS_WHITELIST, fieldWithPath("config.attributeMappings.user_name").optional("NameID").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for SAML is `NameID`.") ), - Stream.of(ALIAS_FIELDS_CREATE), Stream.of(attributeMappingFields) ).flatMap(identity()) .toArray(FieldDescriptor[]::new); - Snippet requestFields = requestFields(idempotentFields); + Snippet requestFields = requestFields((FieldDescriptor[]) ArrayUtils.addAll(idempotentFields, ALIAS_FIELDS_CREATE)); - Snippet responseFields = responseFields((FieldDescriptor[]) ArrayUtils.addAll(idempotentFields, new FieldDescriptor[]{ - VERSION, - ID, - ADDITIONAL_CONFIGURATION, - IDENTITY_ZONE_ID, - CREATED, - LAST_MODIFIED, - fieldWithPath("config.idpEntityAlias").type(STRING).description("This will be set to ``originKey``"), - fieldWithPath("config.zoneId").type(STRING).description("This will be set to the ID of the zone where the provider is being created") - })); + Snippet responseFields = responseFields( + (FieldDescriptor[]) ArrayUtils.addAll( + idempotentFields, + ArrayUtils.addAll( + new FieldDescriptor[]{ + VERSION, + ID, + ADDITIONAL_CONFIGURATION, + IDENTITY_ZONE_ID, + CREATED, + LAST_MODIFIED, + fieldWithPath("config.idpEntityAlias").type(STRING).description("This will be set to ``originKey``"), + fieldWithPath("config.zoneId").type(STRING).description("This will be set to the ID of the zone where the provider is being created") + }, + ALIAS_FIELDS_GET + ) + ) + ); ResultActions resultActionsMetadata = mockMvc.perform(post("/identity-providers") .param("rawConfig", "true") @@ -607,21 +615,36 @@ void createOAuthIdentityProvider() throws Exception { fieldWithPath("config.attributeMappings.user_name").optional("sub").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for OpenID Connect is `sub`"), fieldWithPath("config.groupMappingMode").optional(AbstractExternalOAuthIdentityProviderDefinition.OAuthGroupMappingMode.EXPLICITLY_MAPPED).type(STRING).description("Either ``EXPLICITLY_MAPPED`` in order to map external claim values to OAuth scopes using the group mappings, or ``AS_SCOPES`` to use claim values names as scopes. You need to define also ``external_groups`` for the mapping in order to use this feature.") ), - Stream.of(ALIAS_FIELDS_CREATE), Stream.of(attributeMappingFields) ).flatMap(identity()) .toArray(FieldDescriptor[]::new); - Snippet requestFields = requestFields((FieldDescriptor[]) ArrayUtils.add(idempotentFields, relyingPartySecret)); - Snippet responseFields = responseFields((FieldDescriptor[]) ArrayUtils.addAll(idempotentFields, new FieldDescriptor[]{ - VERSION, - ID, - ADDITIONAL_CONFIGURATION, - IDENTITY_ZONE_ID, - CREATED, - LAST_MODIFIED, - fieldWithPath("config.externalGroupsWhitelist").optional(null).type(ARRAY).description("Not currently used.") - })); + Snippet requestFields = requestFields( + (FieldDescriptor[]) ArrayUtils.addAll( + idempotentFields, + ArrayUtils.add( + ALIAS_FIELDS_CREATE, + relyingPartySecret + ) + ) + ); + Snippet responseFields = responseFields( + (FieldDescriptor[]) ArrayUtils.addAll( + idempotentFields, + ArrayUtils.addAll( + new FieldDescriptor[]{ + VERSION, + ID, + ADDITIONAL_CONFIGURATION, + IDENTITY_ZONE_ID, + CREATED, + LAST_MODIFIED, + fieldWithPath("config.externalGroupsWhitelist").optional(null).type(ARRAY).description("Not currently used.") + }, + ALIAS_FIELDS_GET + ) + ) + ); ResultActions resultActions = mockMvc.perform(post("/identity-providers") .param("rawConfig", "true") @@ -708,20 +731,35 @@ void createOidcIdentityProvider() throws Exception { fieldWithPath("config.prompts[].type").optional(null).type(STRING).description("What kind of field this is (e.g. text or password)"), fieldWithPath("config.prompts[].text").optional(null).type(STRING).description("Actual text displayed on prompt for field") ), - Stream.of(ALIAS_FIELDS_CREATE), Stream.of(attributeMappingFields) ).flatMap(identity()) .toArray(FieldDescriptor[]::new); - Snippet requestFields = requestFields((FieldDescriptor[]) ArrayUtils.add(idempotentFields, relyingPartySecret)); - Snippet responseFields = responseFields((FieldDescriptor[]) ArrayUtils.addAll(idempotentFields, new FieldDescriptor[]{ - VERSION, - ID, - ADDITIONAL_CONFIGURATION, - IDENTITY_ZONE_ID, - CREATED, - LAST_MODIFIED, - })); + Snippet requestFields = requestFields( + (FieldDescriptor[]) ArrayUtils.addAll( + idempotentFields, + ArrayUtils.add( + ALIAS_FIELDS_CREATE, + relyingPartySecret + ) + ) + ); + Snippet responseFields = responseFields( + (FieldDescriptor[]) ArrayUtils.addAll( + idempotentFields, + ArrayUtils.addAll( + new FieldDescriptor[]{ + VERSION, + ID, + ADDITIONAL_CONFIGURATION, + IDENTITY_ZONE_ID, + CREATED, + LAST_MODIFIED, + }, + ALIAS_FIELDS_GET + ) + ) + ); ResultActions resultActions = mockMvc.perform(post("/identity-providers") .param("rawConfig", "true") @@ -853,20 +891,20 @@ void createLDAPProvider(IdentityProvider identit Snippet requestFields = requestFields(fields); Snippet responseFields = responseFields( - (FieldDescriptor[]) ArrayUtils.addAll( - ldapAllFields, - ArrayUtils.addAll( - new FieldDescriptor[]{ + Stream.of( + Stream.of(ldapAllFields), + Stream.of( VERSION, ID, ADDITIONAL_CONFIGURATION, IDENTITY_ZONE_ID, CREATED, LAST_MODIFIED - }, - ALIAS_FIELDS_GET + ), + Stream.of(ALIAS_FIELDS_GET) ) - ) + .flatMap(identity()) + .collect(Collectors.toList()) ); ResultActions resultActions = mockMvc.perform(post("/identity-providers") @@ -996,21 +1034,28 @@ void updateIdentityProvider() throws Exception { fieldWithPath("config.lockoutPolicy.lockoutAfterFailures").constrained("Required when `LockoutPolicy` in the config is not null").type(NUMBER).description("Number of allowed failures before account is locked (defaults to 5).").optional(), fieldWithPath("config.lockoutPolicy.countFailuresWithin").constrained("Required when `LockoutPolicy` in the config is not null").type(NUMBER).description("Number of seconds to lock out an account when lockoutAfterFailures failures is exceeded (defaults to 300).").optional(), fieldWithPath("config.disableInternalUserManagement").optional(null).type(BOOLEAN).description("When set to true, user management is disabled for this provider, defaults to false").optional() - ), - Stream.of(ALIAS_FIELDS_UPDATE) + ) ).flatMap(identity()) .toArray(FieldDescriptor[]::new); - Snippet requestFields = requestFields(idempotentFields); + Snippet requestFields = requestFields((FieldDescriptor[]) ArrayUtils.addAll(idempotentFields, ALIAS_FIELDS_UPDATE)); - Snippet responseFields = responseFields((FieldDescriptor[]) ArrayUtils.addAll(idempotentFields, new FieldDescriptor[]{ - VERSION, - ID, - ADDITIONAL_CONFIGURATION, - IDENTITY_ZONE_ID, - CREATED, - LAST_MODIFIED, - })); + Snippet responseFields = responseFields( + (FieldDescriptor[]) ArrayUtils.addAll( + idempotentFields, + ArrayUtils.addAll( + new FieldDescriptor[]{ + VERSION, + ID, + ADDITIONAL_CONFIGURATION, + IDENTITY_ZONE_ID, + CREATED, + LAST_MODIFIED, + }, + ALIAS_FIELDS_GET + ) + ) + ); mockMvc.perform(put("/identity-providers/{id}", identityProvider.getId()) .param("rawConfig", "true") From 6ba43cf39466be983a2fcd5b3d884cc9e2434bb4 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 15 Jan 2024 09:06:24 +0100 Subject: [PATCH 68/91] Clean up IdentityProviderEndpointDocs --- .../IdentityProviderEndpointDocs.java | 252 ++++++++---------- 1 file changed, 114 insertions(+), 138 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java index c69f90823b8..4cea8109985 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointDocs.java @@ -12,7 +12,6 @@ *******************************************************************************/ package org.cloudfoundry.identity.uaa.mock.providers; -import static java.util.function.Function.identity; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.LDAP; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OAUTH20; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OIDC10; @@ -65,8 +64,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.apache.commons.collections.map.HashedMap; import org.apache.commons.lang.ArrayUtils; @@ -474,29 +471,24 @@ void createSAMLIdentityProvider() throws Exception { IdentityProvider identityProvider = getSamlProvider("SAML"); identityProvider.setSerializeConfigRaw(true); - FieldDescriptor[] idempotentFields = Stream.of( - Stream.of(commonProviderFields), - Stream.of( - fieldWithPath("type").required().description("`saml`"), - fieldWithPath("originKey").required().description("A unique alias for the SAML provider"), - SKIP_SSL_VALIDATION, - STORE_CUSTOM_ATTRIBUTES, - fieldWithPath("config.metaDataLocation").required().type(STRING).description("SAML Metadata - either an XML string or a URL that will deliver XML content"), - fieldWithPath("config.nameID").optional(null).type(STRING).description("The name ID to use for the username, default is \"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\"."), - fieldWithPath("config.assertionConsumerIndex").optional(null).type(NUMBER).description("SAML assertion consumer index, default is 0"), - fieldWithPath("config.metadataTrustCheck").optional(null).type(BOOLEAN).description("Should metadata be validated, defaults to false"), - fieldWithPath("config.showSamlLink").optional(null).type(BOOLEAN).description("Should the SAML login link be displayed on the login page, defaults to false"), - fieldWithPath("config.linkText").constrained("Required if the ``showSamlLink`` is set to true").type(STRING).description("The link text for the SAML IDP on the login page"), - fieldWithPath("config.groupMappingMode").optional(ExternalGroupMappingMode.EXPLICITLY_MAPPED).type(STRING).description("Either ``EXPLICITLY_MAPPED`` in order to map external groups to OAuth scopes using the group mappings, or ``AS_SCOPES`` to use SAML group names as scopes."), - fieldWithPath("config.iconUrl").optional(null).type(STRING).description("Reserved for future use"), - fieldWithPath("config.socketFactoryClassName").optional(null).description("Property is deprecated and value is ignored."), - fieldWithPath("config.authnContext").optional(null).type(ARRAY).description("List of AuthnContextClassRef to include in the SAMLRequest. If not specified no AuthnContext will be requested."), - EXTERNAL_GROUPS_WHITELIST, - fieldWithPath("config.attributeMappings.user_name").optional("NameID").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for SAML is `NameID`.") - ), - Stream.of(attributeMappingFields) - ).flatMap(identity()) - .toArray(FieldDescriptor[]::new); + FieldDescriptor[] idempotentFields = (FieldDescriptor[]) ArrayUtils.addAll(commonProviderFields, ArrayUtils.addAll(new FieldDescriptor[]{ + fieldWithPath("type").required().description("`saml`"), + fieldWithPath("originKey").required().description("A unique alias for the SAML provider"), + SKIP_SSL_VALIDATION, + STORE_CUSTOM_ATTRIBUTES, + fieldWithPath("config.metaDataLocation").required().type(STRING).description("SAML Metadata - either an XML string or a URL that will deliver XML content"), + fieldWithPath("config.nameID").optional(null).type(STRING).description("The name ID to use for the username, default is \"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\"."), + fieldWithPath("config.assertionConsumerIndex").optional(null).type(NUMBER).description("SAML assertion consumer index, default is 0"), + fieldWithPath("config.metadataTrustCheck").optional(null).type(BOOLEAN).description("Should metadata be validated, defaults to false"), + fieldWithPath("config.showSamlLink").optional(null).type(BOOLEAN).description("Should the SAML login link be displayed on the login page, defaults to false"), + fieldWithPath("config.linkText").constrained("Required if the ``showSamlLink`` is set to true").type(STRING).description("The link text for the SAML IDP on the login page"), + fieldWithPath("config.groupMappingMode").optional(ExternalGroupMappingMode.EXPLICITLY_MAPPED).type(STRING).description("Either ``EXPLICITLY_MAPPED`` in order to map external groups to OAuth scopes using the group mappings, or ``AS_SCOPES`` to use SAML group names as scopes."), + fieldWithPath("config.iconUrl").optional(null).type(STRING).description("Reserved for future use"), + fieldWithPath("config.socketFactoryClassName").optional(null).description("Property is deprecated and value is ignored."), + fieldWithPath("config.authnContext").optional(null).type(ARRAY).description("List of AuthnContextClassRef to include in the SAMLRequest. If not specified no AuthnContext will be requested."), + EXTERNAL_GROUPS_WHITELIST, + fieldWithPath("config.attributeMappings.user_name").optional("NameID").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for SAML is `NameID`."), + }, attributeMappingFields)); Snippet requestFields = requestFields((FieldDescriptor[]) ArrayUtils.addAll(idempotentFields, ALIAS_FIELDS_CREATE)); @@ -589,35 +581,30 @@ void createOAuthIdentityProvider() throws Exception { identityProvider.setConfig(definition); identityProvider.setSerializeConfigRaw(true); - FieldDescriptor[] idempotentFields = Stream.of( - Stream.of(commonProviderFields), - Stream.of( - fieldWithPath("type").required().description("`\"" + OAUTH20 + "\"`"), - fieldWithPath("originKey").required().description("A unique alias for a OAuth provider"), - fieldWithPath("config.authUrl").required().type(STRING).description("The OAuth 2.0 authorization endpoint URL"), - fieldWithPath("config.tokenUrl").required().type(STRING).description("The OAuth 2.0 token endpoint URL"), - fieldWithPath("config.tokenKeyUrl").optional(null).type(STRING).description("The URL of the token key endpoint which renders a verification key for validating token signatures"), - fieldWithPath("config.tokenKey").optional(null).type(STRING).description("A verification key for validating token signatures, set to null if a `tokenKeyUrl` is provided."), - fieldWithPath("config.userInfoUrl").optional(null).type(STRING).description("A URL for fetching user info attributes when queried with the obtained token authorization."), - fieldWithPath("config.showLinkText").optional(true).type(BOOLEAN).description("A flag controlling whether a link to this provider's login will be shown on the UAA login page"), - fieldWithPath("config.linkText").optional(null).type(STRING).description("Text to use for the login link to the provider"), - fieldWithPath("config.relyingPartyId").required().type(STRING).description("The client ID which is registered with the external OAuth provider for use by the UAA"), - fieldWithPath("config.skipSslValidation").optional(null).type(BOOLEAN).description("A flag controlling whether SSL validation should be skipped when communicating with the external OAuth server"), - fieldWithPath("config.scopes").optional(null).type(ARRAY).description("What scopes to request on a call to the external OAuth provider"), - fieldWithPath("config.checkTokenUrl").optional(null).type(OBJECT).description("Reserved for future OAuth use."), - fieldWithPath("config.logoutUrl").optional(null).type(OBJECT).description("OAuth 2.0 logout endpoint."), - fieldWithPath("config.responseType").optional("code").type(STRING).description("Response type for the authorize request, will be sent to OAuth server, defaults to `code`"), - fieldWithPath("config.clientAuthInBody").optional(false).type(BOOLEAN).description("Sends the client credentials in the token retrieval call as body parameters instead of a Basic Authorization header."), - fieldWithPath("config.pkce").optional(true).type(BOOLEAN).description("A flag controlling whether PKCE (RFC 7636) is active in authorization code flow when requesting tokens from the external provider."), - fieldWithPath("config.performRpInitiatedLogout").optional(true).type(BOOLEAN).description("A flag controlling whether to log out of the external provider after a successful UAA logout per [OIDC RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)"), - fieldWithPath("config.issuer").optional(null).type(STRING).description("The OAuth 2.0 token issuer. This value is used to validate the issuer inside the token."), - fieldWithPath("config.userPropagationParameter").optional("username").type(STRING).description("Name of the request parameter that is used to pass a known username when redirecting to this identity provider from the account chooser"), - fieldWithPath("config.attributeMappings.user_name").optional("sub").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for OpenID Connect is `sub`"), - fieldWithPath("config.groupMappingMode").optional(AbstractExternalOAuthIdentityProviderDefinition.OAuthGroupMappingMode.EXPLICITLY_MAPPED).type(STRING).description("Either ``EXPLICITLY_MAPPED`` in order to map external claim values to OAuth scopes using the group mappings, or ``AS_SCOPES`` to use claim values names as scopes. You need to define also ``external_groups`` for the mapping in order to use this feature.") - ), - Stream.of(attributeMappingFields) - ).flatMap(identity()) - .toArray(FieldDescriptor[]::new); + FieldDescriptor[] idempotentFields = (FieldDescriptor[]) ArrayUtils.addAll(commonProviderFields, ArrayUtils.addAll(new FieldDescriptor[]{ + fieldWithPath("type").required().description("`\"" + OAUTH20 + "\"`"), + fieldWithPath("originKey").required().description("A unique alias for a OAuth provider"), + fieldWithPath("config.authUrl").required().type(STRING).description("The OAuth 2.0 authorization endpoint URL"), + fieldWithPath("config.tokenUrl").required().type(STRING).description("The OAuth 2.0 token endpoint URL"), + fieldWithPath("config.tokenKeyUrl").optional(null).type(STRING).description("The URL of the token key endpoint which renders a verification key for validating token signatures"), + fieldWithPath("config.tokenKey").optional(null).type(STRING).description("A verification key for validating token signatures, set to null if a `tokenKeyUrl` is provided."), + fieldWithPath("config.userInfoUrl").optional(null).type(STRING).description("A URL for fetching user info attributes when queried with the obtained token authorization."), + fieldWithPath("config.showLinkText").optional(true).type(BOOLEAN).description("A flag controlling whether a link to this provider's login will be shown on the UAA login page"), + fieldWithPath("config.linkText").optional(null).type(STRING).description("Text to use for the login link to the provider"), + fieldWithPath("config.relyingPartyId").required().type(STRING).description("The client ID which is registered with the external OAuth provider for use by the UAA"), + fieldWithPath("config.skipSslValidation").optional(null).type(BOOLEAN).description("A flag controlling whether SSL validation should be skipped when communicating with the external OAuth server"), + fieldWithPath("config.scopes").optional(null).type(ARRAY).description("What scopes to request on a call to the external OAuth provider"), + fieldWithPath("config.checkTokenUrl").optional(null).type(OBJECT).description("Reserved for future OAuth use."), + fieldWithPath("config.logoutUrl").optional(null).type(OBJECT).description("OAuth 2.0 logout endpoint."), + fieldWithPath("config.responseType").optional("code").type(STRING).description("Response type for the authorize request, will be sent to OAuth server, defaults to `code`"), + fieldWithPath("config.clientAuthInBody").optional(false).type(BOOLEAN).description("Sends the client credentials in the token retrieval call as body parameters instead of a Basic Authorization header."), + fieldWithPath("config.pkce").optional(true).type(BOOLEAN).description("A flag controlling whether PKCE (RFC 7636) is active in authorization code flow when requesting tokens from the external provider."), + fieldWithPath("config.performRpInitiatedLogout").optional(true).type(BOOLEAN).description("A flag controlling whether to log out of the external provider after a successful UAA logout per [OIDC RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)"), + fieldWithPath("config.issuer").optional(null).type(STRING).description("The OAuth 2.0 token issuer. This value is used to validate the issuer inside the token."), + fieldWithPath("config.userPropagationParameter").optional("username").type(STRING).description("Name of the request parameter that is used to pass a known username when redirecting to this identity provider from the account chooser"), + fieldWithPath("config.attributeMappings.user_name").optional("sub").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for OpenID Connect is `sub`"), + fieldWithPath("config.groupMappingMode").optional(AbstractExternalOAuthIdentityProviderDefinition.OAuthGroupMappingMode.EXPLICITLY_MAPPED).type(STRING).description("Either ``EXPLICITLY_MAPPED`` in order to map external claim values to OAuth scopes using the group mappings, or ``AS_SCOPES`` to use claim values names as scopes. You need to define also ``external_groups`` for the mapping in order to use this feature."), + }, attributeMappingFields)); Snippet requestFields = requestFields( (FieldDescriptor[]) ArrayUtils.addAll( @@ -691,49 +678,44 @@ void createOidcIdentityProvider() throws Exception { identityProvider.setConfig(definition); identityProvider.setSerializeConfigRaw(true); - FieldDescriptor[] idempotentFields = Stream.of( - Stream.of(commonProviderFields), - Stream.of( - fieldWithPath("type").required().description("`\"" + OIDC10 + "\"`"), - fieldWithPath("originKey").required().description("A unique alias for the OIDC 1.0 provider"), - fieldWithPath("config.discoveryUrl").optional(null).type(STRING).description("The OpenID Connect Discovery URL, typically ends with /.well-known/openid-configurationmit "), - fieldWithPath("config.authUrl").optional().type(STRING).description("The OIDC 1.0 authorization endpoint URL. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL.").attributes(new Attributes.Attribute("constraints", "Required unless `discoveryUrl` is set.")), - fieldWithPath("config.tokenUrl").optional().type(STRING).description("The OIDC 1.0 token endpoint URL. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL.").attributes(new Attributes.Attribute("constraints", "Required unless `discoveryUrl` is set.")), - fieldWithPath("config.tokenKeyUrl").optional(null).type(STRING).description("The URL of the token key endpoint which renders a verification key for validating token signatures. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL.").attributes(new Attributes.Attribute("constraints", "Required unless `discoveryUrl` is set.")), - fieldWithPath("config.tokenKey").optional(null).type(STRING).description("A verification key for validating token signatures. We recommend not setting this as it will not allow for key rotation. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL.").attributes(new Attributes.Attribute("constraints", "Required unless `discoveryUrl` is set.")), - fieldWithPath("config.showLinkText").optional(true).type(BOOLEAN).description("A flag controlling whether a link to this provider's login will be shown on the UAA login page"), - fieldWithPath("config.linkText").optional(null).type(STRING).description("Text to use for the login link to the provider"), - fieldWithPath("config.relyingPartyId").required().type(STRING).description("The client ID which is registered with the external OAuth provider for use by the UAA"), - fieldWithPath("config.skipSslValidation").optional(null).type(BOOLEAN).description("A flag controlling whether SSL validation should be skipped when communicating with the external OAuth server"), - fieldWithPath("config.scopes").optional(null).type(ARRAY).description("What scopes to request on a call to the external OAuth/OpenID provider. For example, can provide " + - "`openid`, `roles`, or `profile` to request ID token, scopes populated in the ID token external groups attribute mappings, or the user profile information, respectively."), - fieldWithPath("config.checkTokenUrl").optional(null).type(OBJECT).description("Reserved for future OAuth/OIDC use."), - fieldWithPath("config.clientAuthInBody").optional(false).type(BOOLEAN).description("Only effective if relyingPartySecret is defined. Sends the client credentials in the token retrieval call as body parameters instead of a Basic Authorization header. It is recommended to set `jwtClientAuthentication:true` instead."), - fieldWithPath("config.pkce").optional(true).type(BOOLEAN).description("A flag controlling whether PKCE (RFC 7636) is active in authorization code flow when requesting tokens from the external provider."), - fieldWithPath("config.performRpInitiatedLogout").optional(true).type(BOOLEAN).description("A flag controlling whether to log out of the external provider after a successful UAA logout per [OIDC RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)"), - fieldWithPath("config.userInfoUrl").optional(null).type(OBJECT).description("Reserved for future OIDC use. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL."), - fieldWithPath("config.logoutUrl").optional(null).type(OBJECT).description("OIDC logout endpoint. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL."), - fieldWithPath("config.responseType").optional("code").type(STRING).description("Response type for the authorize request, defaults to `code`, but can be `code id_token` if the OIDC server can return an id_token as a query parameter in the redirect."), - fieldWithPath("config.issuer").optional(null).type(STRING).description("The OAuth 2.0 token issuer. This value is used to validate the issuer inside the token."), - fieldWithPath("config.userPropagationParameter").optional("username").type(STRING).description("Name of the request parameter that is used to pass a known username when redirecting to this identity provider from the account chooser"), - GROUP_WHITELIST, - fieldWithPath("config.passwordGrantEnabled").optional(false).type(BOOLEAN).description("Enable Resource Owner Password Grant flow for this identity provider."), - fieldWithPath("config.setForwardHeader").optional(false).type(BOOLEAN).description("Only effective if Password Grant enabled. Set X-Forward-For header in Password Grant request to this identity provider."), - fieldWithPath("config.jwtClientAuthentication").optional(null).type(OBJECT).description("UAA 76.5.0 Only effective if relyingPartySecret is not set or null. Creates private_key_jwt client authentication according to OIDC or OAuth2 (RFC 7523) standard. " + - "
            Please note that you can precise the created JWT for client authentication, e.g. if your IdP follows OAuth2 standard according to RFC 7523. For standard OIDC compliance, set true without any further sub-parameters. The supported sub-parameters are " + - "
            • `kid` UAA 76.18.0 Custom key from your defined keys, defaults to `activeKeyId`
            • " + - "
            • `iss` Custom issuer, see RFC 7523, defaults to `relyingPartyId`
            • " + - "
            • `aud` Custom audience, see RFC 7523, defaults to `tokenUrl`
            "), - fieldWithPath("config.attributeMappings.user_name").optional("sub").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for OpenID Connect is `sub`."), - fieldWithPath("config.additionalAuthzParameters").optional(null).type(OBJECT).description("UAA 76.17.0Map of key-value pairs that are added as additional parameters for grant type `authorization_code`. For example, configure an entry with key `token_format` and value `jwt`."), - fieldWithPath("config.prompts[]").optional(null).type(ARRAY).description("List of fields that users are prompted on to the OIDC provider through the password grant flow. Defaults to username, password, and passcode. Any additional prompts beyond username, password, and passcode will be forwarded on to the OIDC provider."), - fieldWithPath("config.prompts[].name").optional(null).type(STRING).description("Name of field"), - fieldWithPath("config.prompts[].type").optional(null).type(STRING).description("What kind of field this is (e.g. text or password)"), - fieldWithPath("config.prompts[].text").optional(null).type(STRING).description("Actual text displayed on prompt for field") - ), - Stream.of(attributeMappingFields) - ).flatMap(identity()) - .toArray(FieldDescriptor[]::new); + FieldDescriptor[] idempotentFields = (FieldDescriptor[]) ArrayUtils.addAll(commonProviderFields, ArrayUtils.addAll(new FieldDescriptor[]{ + fieldWithPath("type").required().description("`\"" + OIDC10 + "\"`"), + fieldWithPath("originKey").required().description("A unique alias for the OIDC 1.0 provider"), + fieldWithPath("config.discoveryUrl").optional(null).type(STRING).description("The OpenID Connect Discovery URL, typically ends with /.well-known/openid-configurationmit "), + fieldWithPath("config.authUrl").optional().type(STRING).description("The OIDC 1.0 authorization endpoint URL. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL.").attributes(new Attributes.Attribute("constraints", "Required unless `discoveryUrl` is set.")), + fieldWithPath("config.tokenUrl").optional().type(STRING).description("The OIDC 1.0 token endpoint URL. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL.").attributes(new Attributes.Attribute("constraints", "Required unless `discoveryUrl` is set.")), + fieldWithPath("config.tokenKeyUrl").optional(null).type(STRING).description("The URL of the token key endpoint which renders a verification key for validating token signatures. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL.").attributes(new Attributes.Attribute("constraints", "Required unless `discoveryUrl` is set.")), + fieldWithPath("config.tokenKey").optional(null).type(STRING).description("A verification key for validating token signatures. We recommend not setting this as it will not allow for key rotation. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL.").attributes(new Attributes.Attribute("constraints", "Required unless `discoveryUrl` is set.")), + fieldWithPath("config.showLinkText").optional(true).type(BOOLEAN).description("A flag controlling whether a link to this provider's login will be shown on the UAA login page"), + fieldWithPath("config.linkText").optional(null).type(STRING).description("Text to use for the login link to the provider"), + fieldWithPath("config.relyingPartyId").required().type(STRING).description("The client ID which is registered with the external OAuth provider for use by the UAA"), + fieldWithPath("config.skipSslValidation").optional(null).type(BOOLEAN).description("A flag controlling whether SSL validation should be skipped when communicating with the external OAuth server"), + fieldWithPath("config.scopes").optional(null).type(ARRAY).description("What scopes to request on a call to the external OAuth/OpenID provider. For example, can provide " + + "`openid`, `roles`, or `profile` to request ID token, scopes populated in the ID token external groups attribute mappings, or the user profile information, respectively."), + fieldWithPath("config.checkTokenUrl").optional(null).type(OBJECT).description("Reserved for future OAuth/OIDC use."), + fieldWithPath("config.clientAuthInBody").optional(false).type(BOOLEAN).description("Only effective if relyingPartySecret is defined. Sends the client credentials in the token retrieval call as body parameters instead of a Basic Authorization header. It is recommended to set `jwtClientAuthentication:true` instead."), + fieldWithPath("config.pkce").optional(true).type(BOOLEAN).description("A flag controlling whether PKCE (RFC 7636) is active in authorization code flow when requesting tokens from the external provider."), + fieldWithPath("config.performRpInitiatedLogout").optional(true).type(BOOLEAN).description("A flag controlling whether to log out of the external provider after a successful UAA logout per [OIDC RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)"), + fieldWithPath("config.userInfoUrl").optional(null).type(OBJECT).description("Reserved for future OIDC use. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL."), + fieldWithPath("config.logoutUrl").optional(null).type(OBJECT).description("OIDC logout endpoint. This can be left blank if a discovery URL is provided. If both are provided, this property overrides the discovery URL."), + fieldWithPath("config.responseType").optional("code").type(STRING).description("Response type for the authorize request, defaults to `code`, but can be `code id_token` if the OIDC server can return an id_token as a query parameter in the redirect."), + fieldWithPath("config.issuer").optional(null).type(STRING).description("The OAuth 2.0 token issuer. This value is used to validate the issuer inside the token."), + fieldWithPath("config.userPropagationParameter").optional("username").type(STRING).description("Name of the request parameter that is used to pass a known username when redirecting to this identity provider from the account chooser"), + GROUP_WHITELIST, + fieldWithPath("config.passwordGrantEnabled").optional(false).type(BOOLEAN).description("Enable Resource Owner Password Grant flow for this identity provider."), + fieldWithPath("config.setForwardHeader").optional(false).type(BOOLEAN).description("Only effective if Password Grant enabled. Set X-Forward-For header in Password Grant request to this identity provider."), + fieldWithPath("config.jwtClientAuthentication").optional(null).type(OBJECT).description("UAA 76.5.0 Only effective if relyingPartySecret is not set or null. Creates private_key_jwt client authentication according to OIDC or OAuth2 (RFC 7523) standard. "+ + "
            Please note that you can precise the created JWT for client authentication, e.g. if your IdP follows OAuth2 standard according to RFC 7523. For standard OIDC compliance, set true without any further sub-parameters. The supported sub-parameters are " + + "
            • `kid` UAA 76.18.0 Custom key from your defined keys, defaults to `activeKeyId`
            • " + + "
            • `iss` Custom issuer, see RFC 7523, defaults to `relyingPartyId`
            • " + + "
            • `aud` Custom audience, see RFC 7523, defaults to `tokenUrl`
            "), + fieldWithPath("config.attributeMappings.user_name").optional("sub").type(STRING).description("Map `user_name` to the attribute for user name in the provider assertion or token. The default for OpenID Connect is `sub`."), + fieldWithPath("config.additionalAuthzParameters").optional(null).type(OBJECT).description("UAA 76.17.0Map of key-value pairs that are added as additional parameters for grant type `authorization_code`. For example, configure an entry with key `token_format` and value `jwt`."), + fieldWithPath("config.prompts[]").optional(null).type(ARRAY).description("List of fields that users are prompted on to the OIDC provider through the password grant flow. Defaults to username, password, and passcode. Any additional prompts beyond username, password, and passcode will be forwarded on to the OIDC provider."), + fieldWithPath("config.prompts[].name").optional(null).type(STRING).description("Name of field"), + fieldWithPath("config.prompts[].type").optional(null).type(STRING).description("What kind of field this is (e.g. text or password)"), + fieldWithPath("config.prompts[].text").optional(null).type(STRING).description("Actual text displayed on prompt for field") + }, attributeMappingFields)); Snippet requestFields = requestFields( (FieldDescriptor[]) ArrayUtils.addAll( @@ -890,22 +872,20 @@ void createLDAPProvider(IdentityProvider identit Snippet requestFields = requestFields(fields); - Snippet responseFields = responseFields( - Stream.of( - Stream.of(ldapAllFields), - Stream.of( - VERSION, - ID, - ADDITIONAL_CONFIGURATION, - IDENTITY_ZONE_ID, - CREATED, - LAST_MODIFIED - ), - Stream.of(ALIAS_FIELDS_GET) - ) - .flatMap(identity()) - .collect(Collectors.toList()) - ); + Snippet responseFields = responseFields((FieldDescriptor[]) ArrayUtils.addAll( + ldapAllFields, + ArrayUtils.addAll( + new FieldDescriptor[]{ + VERSION, + ID, + ADDITIONAL_CONFIGURATION, + IDENTITY_ZONE_ID, + CREATED, + LAST_MODIFIED + }, + ALIAS_FIELDS_GET + ) + )); ResultActions resultActions = mockMvc.perform(post("/identity-providers") .header(IdentityZoneSwitchingFilter.SUBDOMAIN_HEADER, zone.getIdentityZone().getSubdomain()) @@ -948,8 +928,8 @@ void getAllIdentityProviders() throws Exception { fieldWithPath("[].originKey").description("Unique identifier for the identity provider."), fieldWithPath("[].name").description(NAME_DESC), fieldWithPath("[].config").description(CONFIG_DESCRIPTION), - fieldWithPath("[]." + FIELD_ALIAS_ID).description(ALIAS_ID_DESC), - fieldWithPath("[]." + FIELD_ALIAS_ZID).description(fieldWithPath(FIELD_ALIAS_ZID).attributes(key("constraints").value("Optional")).optional().type(STRING)), + fieldWithPath("[]." + FIELD_ALIAS_ID).description(ALIAS_ID_DESC).attributes(key("constraints").value("Optional")).optional().type(STRING), + fieldWithPath("[]." + FIELD_ALIAS_ZID).description(ALIAS_ZID_DESC).attributes(key("constraints").value("Optional")).optional().type(STRING), fieldWithPath("[].version").description(VERSION_DESC), fieldWithPath("[].active").description(ACTIVE_DESC), @@ -1015,28 +995,24 @@ void updateIdentityProvider() throws Exception { identityProvider.setConfig(config); identityProvider.setSerializeConfigRaw(true); - FieldDescriptor[] idempotentFields = Stream.of( - Stream.of(commonProviderFields), - Stream.of( - fieldWithPath("type").required().description("`uaa`"), - fieldWithPath("originKey").required().description("A unique identifier for the IDP. Cannot be updated."), - VERSION.required(), - fieldWithPath("config.passwordPolicy").ignored(), - fieldWithPath("config.passwordPolicy.minLength").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of characters required for password to be considered valid (defaults to 0).").optional(), - fieldWithPath("config.passwordPolicy.maxLength").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Maximum number of characters required for password to be considered valid (defaults to 255).").optional(), - fieldWithPath("config.passwordPolicy.requireUpperCaseCharacter").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of uppercase characters required for password to be considered valid (defaults to 0).").optional(), - fieldWithPath("config.passwordPolicy.requireLowerCaseCharacter").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of lowercase characters required for password to be considered valid (defaults to 0).").optional(), - fieldWithPath("config.passwordPolicy.requireDigit").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of digits required for password to be considered valid (defaults to 0).").optional(), - fieldWithPath("config.passwordPolicy.requireSpecialCharacter").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of special characters required for password to be considered valid (defaults to 0).").optional(), - fieldWithPath("config.passwordPolicy.expirePasswordInMonths").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Number of months after which current password expires (defaults to 0).").optional(), - fieldWithPath("config.passwordPolicy.passwordNewerThan").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("This timestamp value can be used to force change password for every user. If the user's passwordLastModified is older than this value, the password is expired (defaults to null)."), - fieldWithPath("config.lockoutPolicy.lockoutPeriodSeconds").constrained("Required when `LockoutPolicy` in the config is not null").type(NUMBER).description("Number of seconds in which lockoutAfterFailures failures must occur in order for account to be locked (defaults to 3600).").optional(), - fieldWithPath("config.lockoutPolicy.lockoutAfterFailures").constrained("Required when `LockoutPolicy` in the config is not null").type(NUMBER).description("Number of allowed failures before account is locked (defaults to 5).").optional(), - fieldWithPath("config.lockoutPolicy.countFailuresWithin").constrained("Required when `LockoutPolicy` in the config is not null").type(NUMBER).description("Number of seconds to lock out an account when lockoutAfterFailures failures is exceeded (defaults to 300).").optional(), - fieldWithPath("config.disableInternalUserManagement").optional(null).type(BOOLEAN).description("When set to true, user management is disabled for this provider, defaults to false").optional() - ) - ).flatMap(identity()) - .toArray(FieldDescriptor[]::new); + FieldDescriptor[] idempotentFields = (FieldDescriptor[]) ArrayUtils.addAll(commonProviderFields, new FieldDescriptor[]{ + fieldWithPath("type").required().description("`uaa`"), + fieldWithPath("originKey").required().description("A unique identifier for the IDP. Cannot be updated."), + VERSION.required(), + fieldWithPath("config.passwordPolicy").ignored(), + fieldWithPath("config.passwordPolicy.minLength").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of characters required for password to be considered valid (defaults to 0).").optional(), + fieldWithPath("config.passwordPolicy.maxLength").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Maximum number of characters required for password to be considered valid (defaults to 255).").optional(), + fieldWithPath("config.passwordPolicy.requireUpperCaseCharacter").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of uppercase characters required for password to be considered valid (defaults to 0).").optional(), + fieldWithPath("config.passwordPolicy.requireLowerCaseCharacter").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of lowercase characters required for password to be considered valid (defaults to 0).").optional(), + fieldWithPath("config.passwordPolicy.requireDigit").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of digits required for password to be considered valid (defaults to 0).").optional(), + fieldWithPath("config.passwordPolicy.requireSpecialCharacter").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Minimum number of special characters required for password to be considered valid (defaults to 0).").optional(), + fieldWithPath("config.passwordPolicy.expirePasswordInMonths").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("Number of months after which current password expires (defaults to 0).").optional(), + fieldWithPath("config.passwordPolicy.passwordNewerThan").constrained("Required when `passwordPolicy` in the config is not null").type(NUMBER).description("This timestamp value can be used to force change password for every user. If the user's passwordLastModified is older than this value, the password is expired (defaults to null)."), + fieldWithPath("config.lockoutPolicy.lockoutPeriodSeconds").constrained("Required when `LockoutPolicy` in the config is not null").type(NUMBER).description("Number of seconds in which lockoutAfterFailures failures must occur in order for account to be locked (defaults to 3600).").optional(), + fieldWithPath("config.lockoutPolicy.lockoutAfterFailures").constrained("Required when `LockoutPolicy` in the config is not null").type(NUMBER).description("Number of allowed failures before account is locked (defaults to 5).").optional(), + fieldWithPath("config.lockoutPolicy.countFailuresWithin").constrained("Required when `LockoutPolicy` in the config is not null").type(NUMBER).description("Number of seconds to lock out an account when lockoutAfterFailures failures is exceeded (defaults to 300).").optional(), + fieldWithPath("config.disableInternalUserManagement").optional(null).type(BOOLEAN).description("When set to true, user management is disabled for this provider, defaults to false").optional() + }); Snippet requestFields = requestFields((FieldDescriptor[]) ArrayUtils.addAll(idempotentFields, ALIAS_FIELDS_UPDATE)); From fb9f22b20871a15186c03e7f6f6ca4e703cee401 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 15 Jan 2024 09:37:59 +0100 Subject: [PATCH 69/91] Improve test for JdbcIdentityProviderProvisioning#deleteByIdentityZone --- .../JdbcIdentityProviderProvisioning.java | 5 +- ...JdbcIdentityProviderProvisioningTests.java | 68 +++++++++++-------- 2 files changed, 43 insertions(+), 30 deletions(-) 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 2f8bd80c58a..a8117025f20 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 @@ -141,7 +141,7 @@ protected void validate(IdentityProvider provider) { if (!StringUtils.hasText(provider.getIdentityZoneId())) { throw new DataIntegrityViolationException("Identity zone ID must be set."); } - //ensure that SAML IDPs have reduntant fields synchronized + //ensure that SAML IDPs have redundant fields synchronized if (OriginKeys.SAML.equals(provider.getType()) && provider.getConfig() != null) { SamlIdentityProviderDefinition saml = ObjectUtils.castInstance(provider.getConfig(), SamlIdentityProviderDefinition.class); saml.setIdpEntityAlias(provider.getOriginKey()); @@ -150,6 +150,9 @@ protected void validate(IdentityProvider provider) { } } + /** + * Delete all identity providers in the given zone as well as all alias identity providers of them. + */ @Override public int deleteByIdentityZone(String zoneId) { return jdbcTemplate.update(DELETE_IDENTITY_PROVIDER_BY_ZONE_SQL, zoneId, zoneId); diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java index d780509e708..72688c4335a 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/JdbcIdentityProviderProvisioningTests.java @@ -1,6 +1,21 @@ package org.cloudfoundry.identity.uaa.provider; -import org.apache.commons.lang.RandomStringUtils; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; +import static org.cloudfoundry.identity.uaa.zone.IdentityZone.getUaaZoneId; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.sql.Timestamp; +import java.util.List; +import java.util.Map; +import java.util.UUID; + import org.assertj.core.api.Assertions; import org.cloudfoundry.identity.uaa.annotations.WithDatabaseContext; import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent; @@ -15,19 +30,6 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.oauth2.common.util.RandomValueStringGenerator; -import java.sql.Timestamp; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; -import static org.cloudfoundry.identity.uaa.zone.IdentityZone.getUaaZoneId; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.Is.is; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - @WithDatabaseContext class JdbcIdentityProviderProvisioningTests { @@ -67,19 +69,16 @@ void deleteProvidersInZone() { } @Test - void deleteProvidersInUaaZone_WithAlias() { - final IdentityZone mockIdentityZone = mock(IdentityZone.class); - when(mockIdentityZone.getId()).thenReturn(otherZoneId1); - - final String originSuffix = RandomStringUtils.randomAlphabetic(5); + void deleteByIdentityZone_ShouldAlsoDeleteAliasIdentityProviders() { + final String originSuffix = generator.generate(); - // IdP 1: no alias + // IdP 1: created in custom zone, no alias final IdentityProvider idp1 = MultitenancyFixture.identityProvider("origin1-" + originSuffix, otherZoneId1); final IdentityProvider createdIdp1 = jdbcIdentityProviderProvisioning.create(idp1, otherZoneId1); Assertions.assertThat(createdIdp1).isNotNull(); Assertions.assertThat(createdIdp1.getId()).isNotBlank(); - // IdP 2: alias in UAA zone + // IdP 2: created in custom zone, alias in UAA zone final String idp2Id = UUID.randomUUID().toString(); final String idp2AliasId = UUID.randomUUID().toString(); final String origin2 = "origin2-" + originSuffix; @@ -99,17 +98,28 @@ void deleteProvidersInUaaZone_WithAlias() { Assertions.assertThat(createdIdp2Alias.getId()).isNotBlank(); // check if all three entries are present in the DB - Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{otherZoneId1, createdIdp1.getId()}, Integer.class)).isEqualTo(1); - Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{otherZoneId1, createdIdp2.getId()}, Integer.class)).isEqualTo(1); - Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{uaaZoneId, createdIdp2Alias.getId()}, Integer.class)).isEqualTo(1); + assertIdentityProviderExists(createdIdp1.getId(), otherZoneId1); + assertIdentityProviderExists(createdIdp2.getId(), otherZoneId1); + assertIdentityProviderExists(createdIdp2Alias.getId(), uaaZoneId); - // emit custom zone deleted event - jdbcIdentityProviderProvisioning.onApplicationEvent(new EntityDeletedEvent<>(mockIdentityZone, null, otherZoneId1)); + // delete by zone + final int rowsDeleted = jdbcIdentityProviderProvisioning.deleteByIdentityZone(otherZoneId1); + + // number should also include the alias IdP + Assertions.assertThat(rowsDeleted).isEqualTo(3); // check if all three entries are gone - Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{otherZoneId1, createdIdp1.getId()}, Integer.class)).isZero(); - Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{otherZoneId1, createdIdp2.getId()}, Integer.class)).isZero(); - Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{uaaZoneId, createdIdp2Alias.getId()}, Integer.class)).isZero(); + assertIdentityProviderDoesNotExist(createdIdp1.getId(), otherZoneId1); + assertIdentityProviderDoesNotExist(createdIdp2.getId(), otherZoneId1); + assertIdentityProviderDoesNotExist(createdIdp2Alias.getId(), uaaZoneId); + } + + private void assertIdentityProviderExists(final String id, final String zoneId) { + Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{zoneId, id}, Integer.class)).isEqualTo(1); + } + + private void assertIdentityProviderDoesNotExist(final String id, final String zoneId) { + Assertions.assertThat(jdbcTemplate.queryForObject("select count(*) from identity_provider where identity_zone_id=? and id=?", new Object[]{zoneId, id}, Integer.class)).isZero(); } @Test From b12464e1771f9ab9b795c424701529443b7e47f2 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 15 Jan 2024 12:18:10 +0100 Subject: [PATCH 70/91] Improve response code if an IdP with the same origin key already exists in the alias zone --- .../uaa/provider/IdentityProviderTest.java | 2 + .../provider/IdentityProviderEndpoints.java | 38 ++++++++---- .../uaa/provider/IdpAliasFailedException.java | 62 ++++++++++++++++++- ...ityProviderEndpointsAliasMockMvcTests.java | 2 +- 4 files changed, 88 insertions(+), 16 deletions(-) diff --git a/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java b/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java index 0e7c402281e..8d74cace923 100644 --- a/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java +++ b/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java @@ -21,6 +21,8 @@ void testToString_ShouldContainAliasProperties() { config.setIssuer("issuer"); idp.setConfig(config); + assertThat(idp.getAliasId()).isEqualTo("id-of-alias-idp"); + assertThat(idp.getAliasZid()).isEqualTo("custom-zone"); assertThat(idp).hasToString("IdentityProvider{id='12345', identityZoneId='uaa', originKey='some-origin', name='some-name', type='oidc1.0', active=true, aliasId='id-of-alias-idp', aliasZid='custom-zone'}"); } 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 0e551816d6d..a753e94629d 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 @@ -18,6 +18,8 @@ import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OIDC10; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.SAML; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; +import static org.cloudfoundry.identity.uaa.provider.IdpAliasFailedException.Reason.ALIAS_ZONE_DOES_NOT_EXIST; +import static org.cloudfoundry.identity.uaa.provider.IdpAliasFailedException.Reason.ORIGIN_KEY_ALREADY_USED_IN_ALIAS_ZONE; import static org.cloudfoundry.identity.uaa.util.UaaStringUtils.getCleanedUserControlString; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.CONFLICT; @@ -156,6 +158,9 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden }); } catch (final IdpAlreadyExistsException e) { return new ResponseEntity<>(body, CONFLICT); + } catch (final IdpAliasFailedException e) { + logger.warn("Could not create alias for {}", e.getMessage()); + return new ResponseEntity<>(body, e.getResponseCode()); } catch (final Exception e) { logger.warn("Unable to create IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "]", e); return new ResponseEntity<>(body, INTERNAL_SERVER_ERROR); @@ -241,10 +246,16 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str body.setConfig(definition); } - final IdentityProvider updatedIdp = transactionTemplate.execute(txStatus -> { - final IdentityProvider updatedOriginalIdp = identityProviderProvisioning.update(body, zoneId); - return ensureConsistencyOfAliasIdp(updatedOriginalIdp); - }); + final IdentityProvider updatedIdp; + try { + updatedIdp = transactionTemplate.execute(txStatus -> { + final IdentityProvider updatedOriginalIdp = identityProviderProvisioning.update(body, zoneId); + return ensureConsistencyOfAliasIdp(updatedOriginalIdp); + }); + } catch (final IdpAliasFailedException e) { + logger.warn("Could not create alias for {}", e.getMessage()); + return new ResponseEntity<>(body, e.getResponseCode()); + } if (updatedIdp == null) { logger.warn( "IdentityProvider[origin={}; zone={}] - Transaction updating IdP (and alias IdP, if applicable) was not successful, but no exception was thrown.", @@ -451,18 +462,19 @@ private IdentityProvider ensur try { identityZoneProvisioning.retrieve(originalIdp.getAliasZid()); } catch (final ZoneDoesNotExistsException e) { - throw new IdpAliasFailedException(String.format( - "Could not create alias for IdP '%s' in zone '%s', as zone does not exist.", - originalIdp.getId(), - originalIdp.getAliasZid() - ), e); + throw new IdpAliasFailedException(originalIdp, ALIAS_ZONE_DOES_NOT_EXIST, e); } // create new alias IdP in alias zid - final IdentityProvider persistedAliasIdp = identityProviderProvisioning.create( - aliasIdp, - originalIdp.getAliasZid() - ); + final IdentityProvider persistedAliasIdp; + try { + persistedAliasIdp = identityProviderProvisioning.create( + aliasIdp, + originalIdp.getAliasZid() + ); + } catch (final IdpAlreadyExistsException e) { + throw new IdpAliasFailedException(originalIdp, ORIGIN_KEY_ALREADY_USED_IN_ALIAS_ZONE, e); + } // update alias ID in original IdP originalIdp.setAliasId(persistedAliasIdp.getId()); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java index 3d4f7759d7c..d44873e940a 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java @@ -1,9 +1,67 @@ package org.cloudfoundry.identity.uaa.provider; +import java.util.Optional; + import org.cloudfoundry.identity.uaa.error.UaaException; +import org.springframework.http.HttpStatus; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; public class IdpAliasFailedException extends UaaException { - public IdpAliasFailedException(final String msg, final Throwable t) { - super(msg, t); + @NonNull + private final Reason reason; + + public IdpAliasFailedException( + @NonNull final IdentityProvider identityProvider, + @NonNull final Reason reason, + @Nullable final Throwable cause + ) { + super(buildMessage(identityProvider, reason), cause); + this.reason = reason; + } + + public HttpStatus getResponseCode() { + return this.reason.responseCode; + } + + private static String buildMessagePrefix(@NonNull final IdentityProvider idp) { + return String.format( + "IdentityProvider[id=%s,zid=%s,aliasId=%s,aliasZid=%s]", + surroundWithSingleQuotesIfPresent(idp.getId()), + surroundWithSingleQuotesIfPresent(idp.getIdentityZoneId()), + surroundWithSingleQuotesIfPresent(idp.getAliasId()), + surroundWithSingleQuotesIfPresent(idp.getAliasZid()) + ); + } + + private static String buildMessage( + @NonNull final IdentityProvider idp, + @NonNull final Reason reason + ) { + return String.format("%s - %s", buildMessagePrefix(idp), reason.message); + } + + private static String surroundWithSingleQuotesIfPresent(@Nullable final String input) { + return Optional.ofNullable(input).map(it -> "'" + it + "'").orElse(null); + } + + public enum Reason { + ORIGIN_KEY_ALREADY_USED_IN_ALIAS_ZONE( + "An IdP with this origin already exists in the alias zone.", + HttpStatus.CONFLICT + ), + + ALIAS_ZONE_DOES_NOT_EXIST( + "The referenced alias zone does not exist.", + HttpStatus.UNPROCESSABLE_ENTITY + ); + + private final String message; + private final HttpStatus responseCode; + + Reason(final String message, final HttpStatus responseCode) { + this.message = message; + this.responseCode = responseCode; + } } } diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index eebbd6b75a7..dfb3cced415 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -411,7 +411,7 @@ private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final Identi // update the alias ZID to zone 2, where an IdP with this origin already exists -> should fail providerInZone1.setAliasZid(zone2.getId()); - shouldRejectUpdate(zone1, providerInZone1, HttpStatus.INTERNAL_SERVER_ERROR); + shouldRejectUpdate(zone1, providerInZone1, HttpStatus.CONFLICT); } @Test From 05969e10d14f40b8c55642058f0bd3373e3f15c6 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 15 Jan 2024 12:45:27 +0100 Subject: [PATCH 71/91] Add unit tests for additional error cases of IdP endpoints --- .../IdentityProviderEndpointsTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index fd2871cd9ef..647cb82e86c 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -385,6 +385,28 @@ void testUpdateIdpWithExistingAlias_InvalidAliasPropertyChange() throws Metadata Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); } + @Test + void testUpdateIdentityProvider_ShouldRejectInvalidReferenceToAliasInExistingIdp() { + final String customZoneId = UUID.randomUUID().toString(); + + // arrange existing IdP with invalid reference to alias IdP: alias ZID, but alias ID not + final String existingIdpId = UUID.randomUUID().toString(); + final IdentityProvider existingIdp = getLdapDefinition(); + existingIdp.setId(existingIdpId); + existingIdp.setAliasZid(customZoneId); + when(mockIdentityProviderProvisioning.retrieve(existingIdpId, IdentityZone.getUaaZoneId())) + .thenReturn(existingIdp); + + final IdentityProvider requestBody = getLdapDefinition(); + requestBody.setId(existingIdpId); + requestBody.setAliasZid(customZoneId); + requestBody.setName("new-name"); + + Assertions.assertThatIllegalStateException().isThrownBy(() -> + identityProviderEndpoints.updateIdentityProvider(existingIdpId, requestBody, true) + ); + } + @Test void testUpdateIdpWithExistingAlias_ValidChange() throws MetadataProviderException { final String existingIdpId = UUID.randomUUID().toString(); @@ -489,6 +511,18 @@ void testCreateIdentityProvider_AliasPropertiesInvalid() throws MetadataProvider Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); } + @Test + void testCreateIdentityProvider_AliasNotSupportedForType() throws MetadataProviderException { + final String customZoneId = UUID.randomUUID().toString(); + + // alias IdP not supported for IdPs of type LDAP + final IdentityProvider idp = getLdapDefinition(); + idp.setAliasZid(customZoneId); + + final ResponseEntity response = identityProviderEndpoints.createIdentityProvider(idp, true); + Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + } + @Test void testCreateIdentityProvider_ValidAliasProperties() throws MetadataProviderException { // arrange custom zone exists From 276b79eef7a3bead5c6b691d4149161143007a79 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 15 Jan 2024 13:16:10 +0100 Subject: [PATCH 72/91] Add unit test for IdpAliasFailedException --- .../provider/IdentityProviderEndpoints.java | 7 +- .../uaa/provider/IdpAliasFailedException.java | 10 +-- .../provider/IdpAliasFailedExceptionTest.java | 67 +++++++++++++++++++ 3 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedExceptionTest.java 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 a753e94629d..3c609e6bf5f 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 @@ -39,6 +39,7 @@ import java.io.StringWriter; import java.util.Date; import java.util.List; +import java.util.Optional; import java.util.Set; import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent; @@ -160,7 +161,8 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden return new ResponseEntity<>(body, CONFLICT); } catch (final IdpAliasFailedException e) { logger.warn("Could not create alias for {}", e.getMessage()); - return new ResponseEntity<>(body, e.getResponseCode()); + final HttpStatus responseCode = Optional.ofNullable(HttpStatus.resolve(e.getHttpStatus())).orElse(INTERNAL_SERVER_ERROR); + return new ResponseEntity<>(body, responseCode); } catch (final Exception e) { logger.warn("Unable to create IdentityProvider[origin=" + body.getOriginKey() + "; zone=" + body.getIdentityZoneId() + "]", e); return new ResponseEntity<>(body, INTERNAL_SERVER_ERROR); @@ -254,7 +256,8 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str }); } catch (final IdpAliasFailedException e) { logger.warn("Could not create alias for {}", e.getMessage()); - return new ResponseEntity<>(body, e.getResponseCode()); + final HttpStatus responseCode = Optional.ofNullable(HttpStatus.resolve(e.getHttpStatus())).orElse(INTERNAL_SERVER_ERROR); + return new ResponseEntity<>(body, responseCode); } if (updatedIdp == null) { logger.warn( diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java index d44873e940a..84a9d417985 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java @@ -8,20 +8,14 @@ import org.springframework.lang.Nullable; public class IdpAliasFailedException extends UaaException { - @NonNull - private final Reason reason; + private static final String ERROR = "alias_creation_failed"; public IdpAliasFailedException( @NonNull final IdentityProvider identityProvider, @NonNull final Reason reason, @Nullable final Throwable cause ) { - super(buildMessage(identityProvider, reason), cause); - this.reason = reason; - } - - public HttpStatus getResponseCode() { - return this.reason.responseCode; + super(cause, ERROR, buildMessage(identityProvider, reason), reason.responseCode.value()); } private static String buildMessagePrefix(@NonNull final IdentityProvider idp) { diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedExceptionTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedExceptionTest.java new file mode 100644 index 00000000000..186b5eec381 --- /dev/null +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedExceptionTest.java @@ -0,0 +1,67 @@ +package org.cloudfoundry.identity.uaa.provider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; +import static org.cloudfoundry.identity.uaa.provider.IdpAliasFailedException.Reason.ALIAS_ZONE_DOES_NOT_EXIST; +import static org.cloudfoundry.identity.uaa.provider.IdpAliasFailedException.Reason.ORIGIN_KEY_ALREADY_USED_IN_ALIAS_ZONE; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.UUID; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class IdpAliasFailedExceptionTest { + private static final String IDP_ID = UUID.randomUUID().toString(); + private static final String IDZ_ID = UUID.randomUUID().toString(); + private static final String ALIAS_ID = UUID.randomUUID().toString(); + private static final String ALIAS_ZID = UAA; + + private static IdentityProvider mockIdentityProvider; + + @BeforeAll + static void beforeAll() { + mockIdentityProvider = mock(IdentityProvider.class); + when(mockIdentityProvider.getId()).thenReturn(IDP_ID); + when(mockIdentityProvider.getIdentityZoneId()).thenReturn(IDZ_ID); + when(mockIdentityProvider.getAliasId()).thenReturn(ALIAS_ID); + when(mockIdentityProvider.getAliasZid()).thenReturn(ALIAS_ZID); + } + + @Test + void testOriginKeyAlreadyUserInAliasZone() { + final IdpAliasFailedException exception = new IdpAliasFailedException( + mockIdentityProvider, + ORIGIN_KEY_ALREADY_USED_IN_ALIAS_ZONE, + null + ); + + assertThat(exception.getMessage()).isEqualTo( + "IdentityProvider[id='%s',zid='%s',aliasId='%s',aliasZid='%s'] - An IdP with this origin already exists in the alias zone.".formatted( + mockIdentityProvider.getId(), + mockIdentityProvider.getIdentityZoneId(), + mockIdentityProvider.getAliasId(), + mockIdentityProvider.getAliasZid() + ) + ); + } + + @Test + void testAliasZoneDoesNotExist() { + final IdpAliasFailedException exception = new IdpAliasFailedException( + mockIdentityProvider, + ALIAS_ZONE_DOES_NOT_EXIST, + null + ); + + assertThat(exception.getMessage()).isEqualTo( + "IdentityProvider[id='%s',zid='%s',aliasId='%s',aliasZid='%s'] - The referenced alias zone does not exist.".formatted( + mockIdentityProvider.getId(), + mockIdentityProvider.getIdentityZoneId(), + mockIdentityProvider.getAliasId(), + mockIdentityProvider.getAliasZid() + ) + ); + } +} \ No newline at end of file From e17f89c5cdd970668ded6c245c6429759357daa1 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 15 Jan 2024 13:36:12 +0100 Subject: [PATCH 73/91] Add further unit tests --- .../uaa/provider/IdentityProviderTest.java | 17 ++++++++++ .../IdentityProviderEndpointsTest.java | 32 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java b/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java index 8d74cace923..c39ec5d2ffa 100644 --- a/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java +++ b/model/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderTest.java @@ -26,6 +26,23 @@ void testToString_ShouldContainAliasProperties() { assertThat(idp).hasToString("IdentityProvider{id='12345', identityZoneId='uaa', originKey='some-origin', name='some-name', type='oidc1.0', active=true, aliasId='id-of-alias-idp', aliasZid='custom-zone'}"); } + @Test + void testToString_AliasPropertiesAndIdzIdNull() { + final IdentityProvider idp = new IdentityProvider<>(); + idp.setId("12345"); + idp.setName("some-name"); + idp.setOriginKey("some-origin"); + idp.setAliasZid(null); + idp.setAliasId(null); + idp.setActive(true); + idp.setIdentityZoneId(null); + final OIDCIdentityProviderDefinition config = new OIDCIdentityProviderDefinition(); + config.setIssuer("issuer"); + idp.setConfig(config); + + assertThat(idp).hasToString("IdentityProvider{id='12345', identityZoneId=null, originKey='some-origin', name='some-name', type='oidc1.0', active=true, aliasId=null, aliasZid=null}"); + } + @Test void testEqualsAndHashCode() { final String customZoneId = "custom-zone"; diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index 647cb82e86c..a862dd9ef0d 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -739,6 +739,38 @@ void testDeleteIdpWithAlias() { Assertions.assertThat(((IdentityProvider) secondEvent.getSource()).getId()).isEqualTo(aliasIdpId); } + @Test + void testDeleteIdpWithAlias_DanglingReference() { + final String idpId = UUID.randomUUID().toString(); + final String aliasIdpId = UUID.randomUUID().toString(); + final String customZoneId = UUID.randomUUID().toString(); + + final IdentityProvider idp = new IdentityProvider<>(); + idp.setType(OIDC10); + idp.setId(idpId); + idp.setIdentityZoneId(UAA); + idp.setAliasId(aliasIdpId); + idp.setAliasZid(customZoneId); + when(mockIdentityProviderProvisioning.retrieve(idpId, UAA)).thenReturn(idp); + + // alias IdP is not present -> dangling reference + + final ApplicationEventPublisher mockEventPublisher = mock(ApplicationEventPublisher.class); + identityProviderEndpoints.setApplicationEventPublisher(mockEventPublisher); + doNothing().when(mockEventPublisher).publishEvent(any()); + + identityProviderEndpoints.deleteIdentityProvider(idpId, true); + final ArgumentCaptor> entityDeletedEventCaptor = ArgumentCaptor.forClass(EntityDeletedEvent.class); + + // should only be called for the original IdP + verify(mockEventPublisher, times(1)).publishEvent(entityDeletedEventCaptor.capture()); + + final EntityDeletedEvent firstEvent = entityDeletedEventCaptor.getAllValues().get(0); + Assertions.assertThat(firstEvent).isNotNull(); + Assertions.assertThat(firstEvent.getIdentityZoneId()).isEqualTo(UAA); + Assertions.assertThat(((IdentityProvider) firstEvent.getSource()).getId()).isEqualTo(idpId); + } + @Test void testDeleteIdentityProviderNotExisting() { String zoneId = IdentityZone.getUaaZoneId(); From f438c592c25230b69a29e19b377b36f8d48491b4 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 15 Jan 2024 14:11:44 +0100 Subject: [PATCH 74/91] Replace SAML IdPs with OIDC IdPs in IdentityProviderEndpointsAliasMockMvcTests --- ...ityProviderEndpointsAliasMockMvcTests.java | 79 +++++++++---------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index dfb3cced415..37db97c20fd 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -1,11 +1,9 @@ package org.cloudfoundry.identity.uaa.mock.providers; -import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import static org.assertj.core.api.Assertions.assertThat; -import static org.cloudfoundry.identity.uaa.constants.OriginKeys.SAML; +import static org.cloudfoundry.identity.uaa.constants.OriginKeys.OIDC10; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -15,7 +13,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.util.StringUtils.hasText; -import java.util.Arrays; +import java.net.MalformedURLException; +import java.net.URL; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -30,10 +29,9 @@ import org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.IdentityProvider; import org.cloudfoundry.identity.uaa.provider.JdbcIdentityProviderProvisioning; +import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition; 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.BootstrapSamlIdentityProviderDataTests; import org.cloudfoundry.identity.uaa.scim.ScimUser; import org.cloudfoundry.identity.uaa.test.TestClient; import org.cloudfoundry.identity.uaa.util.AlphanumericRandomValueStringGenerator; @@ -104,7 +102,7 @@ void shouldAccept_CreateAliasIdp_CustomToUaaZone() throws Exception { private void shouldAccept_CreateAliasIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { // build IdP in zone1 with aliasZid set to zone2 - final IdentityProvider provider = buildSamlIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); + final IdentityProvider provider = buildOidcIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); // create IdP in zone1 final IdentityProvider originalIdp = createIdp(zone1, provider); @@ -134,7 +132,7 @@ void shouldReject_IdzAndAliasZidAreEqual_CustomZone() throws Exception { } private void shouldReject_IdzAndAliasZidAreEqual(final IdentityZone zone) throws Exception { - final IdentityProvider idp = buildSamlIdpWithAliasProperties(zone.getId(), null, zone.getId()); + final IdentityProvider idp = buildOidcIdpWithAliasProperties(zone.getId(), null, zone.getId()); shouldRejectCreation(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } @@ -156,20 +154,20 @@ private void shouldReject_AliasNotSupportedForIdpType(final IdentityZone zone1, @Test void shouldReject_NeitherIdzNorAliasZidIsUaa() throws Exception { final IdentityZone otherCustomZone = MockMvcUtils.createZoneUsingWebRequest(mockMvc, identityToken); - final IdentityProvider idp = buildSamlIdpWithAliasProperties(customZone.getId(), null, otherCustomZone.getId()); + final IdentityProvider idp = buildOidcIdpWithAliasProperties(customZone.getId(), null, otherCustomZone.getId()); shouldRejectCreation(customZone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } @Test void shouldReject_AliasIdIsSet() throws Exception { final String aliasId = UUID.randomUUID().toString(); - final IdentityProvider idp = buildSamlIdpWithAliasProperties(customZone.getId(), aliasId, IdentityZone.getUaaZoneId()); + final IdentityProvider idp = buildOidcIdpWithAliasProperties(customZone.getId(), aliasId, IdentityZone.getUaaZoneId()); shouldRejectCreation(customZone, idp, HttpStatus.UNPROCESSABLE_ENTITY); } @Test void shouldReject_IdzReferencedInAliasZidDoesNotExist() throws Exception { - final IdentityProvider provider = buildSamlIdpWithAliasProperties( + final IdentityProvider provider = buildOidcIdpWithAliasProperties( IdentityZone.getUaaZoneId(), null, UUID.randomUUID().toString() // does not exist @@ -191,14 +189,14 @@ private void shouldReject_IdpWithOriginAlreadyExistsInAliasZone(final IdentityZo // create IdP with origin key in zone 1 final IdentityProvider createdIdp1 = createIdp( zone1, - buildSamlIdpWithAliasProperties(zone1.getId(), null, null) + buildOidcIdpWithAliasProperties(zone1.getId(), null, null) ); assertThat(createdIdp1).isNotNull(); // then, create an IdP in zone 2 with the same origin key for which an alias in zone 1 should be created -> should fail shouldRejectCreation( zone2, - buildIdpWithAliasProperties(zone2.getId(), null, zone1.getId(), createdIdp1.getOriginKey(), SAML), + buildIdpWithAliasProperties(zone2.getId(), null, zone1.getId(), createdIdp1.getOriginKey(), OIDC10), HttpStatus.CONFLICT ); } @@ -232,7 +230,7 @@ private void shouldAccept_ShouldCreateAliasIdp(final IdentityZone zone1, final I // create regular idp without alias properties in zone 1 final IdentityProvider existingIdpWithoutAlias = createIdp( zone1, - buildSamlIdpWithAliasProperties(zone1.getId(), null, null) + buildOidcIdpWithAliasProperties(zone1.getId(), null, null) ); assertThat(existingIdpWithoutAlias).isNotNull(); assertThat(existingIdpWithoutAlias.getId()).isNotBlank(); @@ -357,7 +355,7 @@ void shouldReject_OnlyAliasIdSet_CustomZone() throws Exception { } private void shouldReject_OnlyAliasIdSet(final IdentityZone zone) throws Exception { - final IdentityProvider idp = buildSamlIdpWithAliasProperties(zone.getId(), null, null); + final IdentityProvider idp = buildOidcIdpWithAliasProperties(zone.getId(), null, null); final IdentityProvider createdProvider = createIdp(zone, idp); assertThat(createdProvider.getAliasZid()).isBlank(); createdProvider.setAliasId(UUID.randomUUID().toString()); @@ -396,7 +394,7 @@ void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone_CustomToUaaZone() th private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final IdentityZone zone1, final IdentityZone zone2) throws Exception { // create IdP with origin key in zone 2 - final IdentityProvider existingIdpInZone2 = buildSamlIdpWithAliasProperties(zone2.getId(), null, null); + final IdentityProvider existingIdpInZone2 = buildOidcIdpWithAliasProperties(zone2.getId(), null, null); createIdp(zone2, existingIdpInZone2); // create IdP with same origin key in zone 1 @@ -405,7 +403,7 @@ private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final Identi null, null, existingIdpInZone2.getOriginKey(), // same origin key - SAML + OIDC10 ); final IdentityProvider providerInZone1 = createIdp(zone1, idp); @@ -418,7 +416,7 @@ private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final Identi void shouldReject_IdpInCustomZone_AliasToOtherCustomZone() throws Exception { final IdentityProvider idpInCustomZone = createIdp( customZone, - buildSamlIdpWithAliasProperties(customZone.getId(), null, null) + buildOidcIdpWithAliasProperties(customZone.getId(), null, null) ); // try to create an alias in another custom zone -> should fail @@ -439,7 +437,7 @@ void shouldReject_AliasZidSetToSameZone_CustomZone() throws Exception { private void shouldReject_AliasZidSetToSameZone(final IdentityZone zone) throws Exception { final IdentityProvider idp = createIdp( zone, - buildSamlIdpWithAliasProperties(zone.getId(), null, null) + buildOidcIdpWithAliasProperties(zone.getId(), null, null) ); idp.setAliasZid(zone.getId()); shouldRejectUpdate(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); @@ -605,11 +603,9 @@ private void assertIdpReferencesOtherIdp(final IdentityProvider idp, final Id } private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final IdentityProvider aliasIdp) { - // apart from the zone ID, the configs should be identical - final SamlIdentityProviderDefinition originalIdpConfig = (SamlIdentityProviderDefinition) idp.getConfig(); - originalIdpConfig.setZoneId(null); - final SamlIdentityProviderDefinition aliasIdpConfig = (SamlIdentityProviderDefinition) aliasIdp.getConfig(); - aliasIdpConfig.setZoneId(null); + // the configs should be identical + final OIDCIdentityProviderDefinition originalIdpConfig = (OIDCIdentityProviderDefinition) idp.getConfig(); + final OIDCIdentityProviderDefinition aliasIdpConfig = (OIDCIdentityProviderDefinition) aliasIdp.getConfig(); assertThat(aliasIdpConfig).isEqualTo(originalIdpConfig); // check if remaining properties are equal @@ -619,7 +615,7 @@ private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final } private IdentityProvider createIdpWithAlias(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final IdentityProvider provider = buildSamlIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); + final IdentityProvider provider = buildOidcIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); final IdentityProvider createdOriginalIdp = createIdp(zone1, provider); assertThat(createdOriginalIdp.getAliasId()).isNotBlank(); assertThat(createdOriginalIdp.getAliasZid()).isNotBlank(); @@ -719,13 +715,13 @@ private static List getScopesForZone(final String zoneId, final String.. return Stream.of(scopes).map(scope -> String.format("zones.%s.%s", zoneId, scope)).collect(toList()); } - private static IdentityProvider buildSamlIdpWithAliasProperties( + private static IdentityProvider buildOidcIdpWithAliasProperties( final String idzId, final String aliasId, final String aliasZid ) { final String originKey = RANDOM_STRING_GENERATOR.generate(); - return buildIdpWithAliasProperties(idzId, aliasId, aliasZid, originKey, SAML); + return buildIdpWithAliasProperties(idzId, aliasId, aliasZid, originKey, OIDC10); } private IdentityProvider buildUaaIdpWithAliasProperties( @@ -760,19 +756,22 @@ private static IdentityProvider buildIdpWithAliasProperties( private static AbstractIdentityProviderDefinition buildIdpDefinition(final String originKey, final String type) { switch (type) { - case SAML: - final String metadata = String.format( - BootstrapSamlIdentityProviderDataTests.xmlWithoutID, - "http://localhost:9999/metadata/" + originKey - ); - final SamlIdentityProviderDefinition samlDefinition = new SamlIdentityProviderDefinition() - .setMetaDataLocation(metadata) - .setLinkText("Test SAML Provider"); - samlDefinition.setEmailDomain(Arrays.asList("test.com", "test2.com")); - samlDefinition.setExternalGroupsWhitelist(singletonList("value")); - samlDefinition.setAttributeMappings(singletonMap("given_name", "first_name")); - - return samlDefinition; + case OIDC10: + final OIDCIdentityProviderDefinition definition = new OIDCIdentityProviderDefinition(); + try { + return definition + .setAuthUrl(new URL("https://www.example.com/oauth/authorize")) + .setLinkText("link text") + .setRelyingPartyId("relying-party-id") + .setRelyingPartySecret("relying-party-secret") + .setShowLinkText(true) + .setSkipSslValidation(true) + .setTokenKey("key") + .setTokenKeyUrl(new URL("https://www.example.com/token_keys")) + .setTokenUrl(new URL("https://wwww.example.com/oauth/token")); + } catch (final MalformedURLException e) { + throw new RuntimeException(e); + } case UAA: final PasswordPolicy passwordPolicy = new PasswordPolicy(); passwordPolicy.setExpirePasswordInMonths(1); From da2773e6f3676633bc34d7f8ebc931ea47437220 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 15 Jan 2024 17:13:45 +0100 Subject: [PATCH 75/91] Add tests about redacting relying party secret --- ...ityProviderEndpointsAliasMockMvcTests.java | 79 +++++++++++++++++-- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 37db97c20fd..634456c1097 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -26,6 +26,7 @@ import org.cloudfoundry.identity.uaa.DefaultTestContext; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; import org.cloudfoundry.identity.uaa.oauth.token.TokenConstants; +import org.cloudfoundry.identity.uaa.provider.AbstractExternalOAuthIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.IdentityProvider; import org.cloudfoundry.identity.uaa.provider.JdbcIdentityProviderProvisioning; @@ -119,6 +120,12 @@ private void shouldAccept_CreateAliasIdp(final IdentityZone zone1, final Identit // check if aliasId in first IdP is equal to the ID of the alias IdP assertThat(aliasIdp.get().getId()).isEqualTo(originalIdp.getAliasId()); + + // check if both have the same non-empty relying party secret + assertIdpAndAliasHaveSameRelyingPartySecretInDb(originalIdp); + + // check if the returned IdP has a redacted relying party secret + assertRelyingPartySecretIsRedacted(originalIdp); } @Test @@ -281,6 +288,12 @@ private void shouldAccept_OtherPropertiesOfIdpWithAliasAreChanged(final Identity assertThat(aliasIdp).isPresent(); assertIdpReferencesOtherIdp(aliasIdp.get(), updatedOriginalIdp); assertThat(aliasIdp.get().getName()).isNotBlank().isEqualTo(newName); + + // check if both have the same non-empty relying party secret in the DB + assertIdpAndAliasHaveSameRelyingPartySecretInDb(updatedOriginalIdp); + + // check if the returned IdP has a redacted relying party secret + assertRelyingPartySecretIsRedacted(updatedOriginalIdp); } @Test @@ -311,6 +324,12 @@ private void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewAliasIdp(final assertThat(aliasIdp).isPresent(); assertIdpReferencesOtherIdp(updatedIdp, aliasIdp.get()); assertOtherPropertiesAreEqual(updatedIdp, aliasIdp.get()); + + // check if both have the same non-empty relying party secret + assertIdpAndAliasHaveSameRelyingPartySecretInDb(updatedIdp); + + // check if the returned IdP has a redacted relying party secret + assertRelyingPartySecretIsRedacted(updatedIdp); } @ParameterizedTest @@ -515,15 +534,9 @@ private void shouldRejectUpdate(final IdentityZone zone, final IdentityProvider< } } - private void deleteIdpViaDb(final String originKey, final String zoneId) { - final JdbcIdentityProviderProvisioning identityProviderProvisioning = webApplicationContext - .getBean(JdbcIdentityProviderProvisioning.class); - final int rowsDeleted = identityProviderProvisioning.deleteByOrigin(originKey, zoneId); - assertThat(rowsDeleted).isEqualTo(1); - } - @Nested class Delete { + @Test void shouldAlsoDeleteAliasIdp_UaaToCustomZone() throws Exception { shouldAlsoDeleteAliasIdp(IdentityZone.getUaa(), customZone); @@ -593,6 +606,7 @@ private void assertIdpDoesNotExist(final IdentityZone zone, final String id) thr final Optional> idp = readIdpFromZoneIfExists(zone.getId(), id); assertThat(idp).isNotPresent(); } + } private void assertIdpReferencesOtherIdp(final IdentityProvider idp, final IdentityProvider referencedIdp) { @@ -711,6 +725,57 @@ private List> readAllIdpsInZone(final IdentityZone zone) thr }); } + private void deleteIdpViaDb(final String originKey, final String zoneId) { + final JdbcIdentityProviderProvisioning identityProviderProvisioning = webApplicationContext + .getBean(JdbcIdentityProviderProvisioning.class); + final int rowsDeleted = identityProviderProvisioning.deleteByOrigin(originKey, zoneId); + assertThat(rowsDeleted).isEqualTo(1); + } + + private void assertIdpAndAliasHaveSameRelyingPartySecretInDb(final IdentityProvider originalIdp) { + assertThat(originalIdp.getType()).isEqualTo(OIDC10); + assertThat(originalIdp.getAliasId()).isNotNull().isNotBlank(); + assertThat(originalIdp.getAliasZid()).isNotNull().isNotBlank(); + + final Optional relyingPartySecretOriginalIdpOpt = readIdpViaDb(originalIdp.getId(), originalIdp.getIdentityZoneId()) + .map(IdentityProvider::getConfig) + .map(it -> (AbstractExternalOAuthIdentityProviderDefinition) it) + .map(AbstractExternalOAuthIdentityProviderDefinition::getRelyingPartySecret); + assertThat(relyingPartySecretOriginalIdpOpt).isPresent(); + final String relyingPartySecretOriginalIdp = relyingPartySecretOriginalIdpOpt.get(); + assertThat(relyingPartySecretOriginalIdp).isNotBlank(); + + final Optional relyingPartySecretAliasIdpOpt = readIdpViaDb(originalIdp.getAliasId(), originalIdp.getAliasZid()) + .map(IdentityProvider::getConfig) + .map(it -> (AbstractExternalOAuthIdentityProviderDefinition) it) + .map(AbstractExternalOAuthIdentityProviderDefinition::getRelyingPartySecret); + assertThat(relyingPartySecretAliasIdpOpt).isPresent(); + final String relyingPartySecretAliasIdp = relyingPartySecretAliasIdpOpt.get(); + assertThat(relyingPartySecretAliasIdp).isNotBlank(); + + assertThat(relyingPartySecretOriginalIdp).isEqualTo(relyingPartySecretAliasIdp); + } + + private Optional> readIdpViaDb(final String id, final String zoneId) { + final JdbcIdentityProviderProvisioning identityProviderProvisioning = webApplicationContext + .getBean(JdbcIdentityProviderProvisioning.class); + final IdentityProvider idp; + try { + idp = identityProviderProvisioning.retrieve(id, zoneId); + } catch (final Exception e) { + return Optional.empty(); + } + return Optional.of(idp); + } + + private void assertRelyingPartySecretIsRedacted(final IdentityProvider identityProvider) { + assertThat(identityProvider.getType()).isEqualTo(OIDC10); + final Optional> config = Optional.ofNullable(identityProvider.getConfig()) + .map(it -> (AbstractExternalOAuthIdentityProviderDefinition) it); + assertThat(config).isPresent(); + assertThat(config.get().getRelyingPartySecret()).isBlank(); + } + private static List getScopesForZone(final String zoneId, final String... scopes) { return Stream.of(scopes).map(scope -> String.format("zones.%s.%s", zoneId, scope)).collect(toList()); } From 4e3a82700b8e3881d030499da529e0cd61849b5a Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 24 Jan 2024 13:56:42 +0100 Subject: [PATCH 76/91] Add feature flag to uaa configuration --- .../provider/IdentityProviderEndpoints.java | 20 ++++++++++--------- uaa/src/main/resources/uaa.yml | 2 ++ 2 files changed, 13 insertions(+), 9 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 3c609e6bf5f..91822e4a0f6 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 @@ -57,6 +57,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.dao.EmptyResultDataAccessException; @@ -90,6 +91,7 @@ public class IdentityProviderEndpoints implements ApplicationEventPublisherAware */ private static final Set IDP_TYPES_ALIAS_SUPPORTED = Set.of(SAML, OAUTH20, OIDC10); + private final boolean aliasEntitiesEnabled; private final IdentityProviderProvisioning identityProviderProvisioning; private final ScimGroupExternalMembershipManager scimGroupExternalMembershipManager; private final ScimGroupProvisioning scimGroupProvisioning; @@ -115,7 +117,8 @@ public IdentityProviderEndpoints( final @Qualifier("identityProviderConfigValidator") IdentityProviderConfigValidator configValidator, final IdentityZoneManager identityZoneManager, final @Qualifier("identityZoneProvisioning") IdentityZoneProvisioning identityZoneProvisioning, - final @Qualifier("transactionManager") PlatformTransactionManager transactionManager + final @Qualifier("transactionManager") PlatformTransactionManager transactionManager, + final @Value("${uaa.features.aliasEntitiesEnabled:false}") boolean aliasEntitiesEnabled ) { this.identityProviderProvisioning = identityProviderProvisioning; this.scimGroupExternalMembershipManager = scimGroupExternalMembershipManager; @@ -125,6 +128,7 @@ public IdentityProviderEndpoints( this.identityZoneManager = identityZoneManager; this.identityZoneProvisioning = identityZoneProvisioning; this.transactionTemplate = new TransactionTemplate(transactionManager); + this.aliasEntitiesEnabled = aliasEntitiesEnabled; } @RequestMapping(method = POST) @@ -484,18 +488,16 @@ private IdentityProvider ensur return identityProviderProvisioning.update(originalIdp, originalIdp.getIdentityZoneId()); } - private IdentityProvider retrieveAliasIdp(final IdentityProvider originalIdp) { + @Nullable + private IdentityProvider retrieveAliasIdp(final IdentityProvider idp) { try { - return identityProviderProvisioning.retrieve( - originalIdp.getAliasId(), - originalIdp.getAliasZid() - ); + return identityProviderProvisioning.retrieve(idp.getAliasId(), idp.getAliasZid()); } catch (final EmptyResultDataAccessException e) { logger.warn( "The IdP referenced in the 'aliasId' ('{}') and 'aliasZid' ('{}') of the IdP '{}' does not exist.", - originalIdp.getAliasId(), - originalIdp.getAliasZid(), - originalIdp.getId() + idp.getAliasId(), + idp.getAliasZid(), + idp.getId() ); return null; } diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index 0b026ed5c9e..0cdb0f16bab 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -553,6 +553,8 @@ uaa: - GET - HEAD - OPTIONS + features: +# aliasEntitiesEnabled: true #smtp: # host: localhost From 34b141bcc168aef1a5c1cbc7f4ab69c1a9da40be Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 24 Jan 2024 14:39:35 +0100 Subject: [PATCH 77/91] Add handling of feature flag to alias property validation --- .../uaa/provider/IdentityProviderEndpoints.java | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 91822e4a0f6..3c4db0adf0d 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 @@ -366,6 +366,11 @@ private boolean aliasPropertiesAreValid( // if the IdP already has an alias, the alias properties must not be changed final boolean idpAlreadyHasAlias = existingIdp != null && hasText(existingIdp.getAliasZid()); if (idpAlreadyHasAlias) { + if (!aliasEntitiesEnabled) { + // if the feature is disabled, we only allow setting both alias properties to null + return !hasText(requestBody.getAliasId()) && !hasText(requestBody.getAliasZid()); + } + if (!hasText(existingIdp.getAliasId())) { // at this point, we expect both properties to be set -> if not, the IdP is in an inconsistent state throw new IllegalStateException(String.format( @@ -390,6 +395,12 @@ private boolean aliasPropertiesAreValid( return true; } + /* At this point, we know that a new alias entity should be created. + * -> check if the creation of alias entities is enabled */ + if (!aliasEntitiesEnabled) { + return false; + } + // check if aliases are supported for this IdP type if (!IDP_TYPES_ALIAS_SUPPORTED.contains(requestBody.getType())) { return false; From ccdf1ab48224cab1c535dada7c970d7559f7fc31 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Wed, 24 Jan 2024 14:46:10 +0100 Subject: [PATCH 78/91] Add handling of alias feature flag to IdP deletion --- .../provider/IdentityProviderEndpoints.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 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 3c4db0adf0d..4f8b6df750c 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 @@ -201,19 +201,29 @@ public ResponseEntity deleteIdentityProvider(@PathVariable Str publisher.publishEvent(new EntityDeletedEvent<>(existing, authentication, identityZoneId)); redactSensitiveData(existing); - // delete alias IdP if alias fields are set if (hasText(existing.getAliasZid()) && hasText(existing.getAliasId())) { final IdentityProvider aliasIdp = retrieveAliasIdp(existing); - if (aliasIdp != null) { - aliasIdp.setSerializeConfigRaw(rawConfig); - publisher.publishEvent(new EntityDeletedEvent<>(aliasIdp, authentication, identityZoneId)); - } else { + if (aliasIdp == null) { logger.warn( "Alias IdP referenced in IdentityProvider[origin={}; zone={}}] not found, skipping deletion of alias IdP.", existing.getOriginKey(), existing.getIdentityZoneId() ); + return new ResponseEntity<>(existing, OK); } + + if (!aliasEntitiesEnabled) { + // if alias entities are not enabled, just break the reference + aliasIdp.setAliasId(null); + aliasIdp.setAliasZid(null); + identityProviderProvisioning.update(aliasIdp, aliasIdp.getIdentityZoneId()); + + return new ResponseEntity<>(existing, OK); + } + + // also delete the alias IdP + aliasIdp.setSerializeConfigRaw(rawConfig); + publisher.publishEvent(new EntityDeletedEvent<>(aliasIdp, authentication, identityZoneId)); } return new ResponseEntity<>(existing, OK); From 9efb1187d369e551bf60d4e24204b72d223e7139 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 25 Jan 2024 13:31:29 +0100 Subject: [PATCH 79/91] Add breaking of reference during create and update --- .../provider/IdentityProviderEndpoints.java | 65 +++++++++++++++++-- .../uaa/provider/IdpAliasFailedException.java | 5 +- 2 files changed, 64 insertions(+), 6 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 4f8b6df750c..5e459579fcc 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 @@ -19,6 +19,7 @@ import static org.cloudfoundry.identity.uaa.constants.OriginKeys.SAML; import static org.cloudfoundry.identity.uaa.constants.OriginKeys.UAA; import static org.cloudfoundry.identity.uaa.provider.IdpAliasFailedException.Reason.ALIAS_ZONE_DOES_NOT_EXIST; +import static org.cloudfoundry.identity.uaa.provider.IdpAliasFailedException.Reason.COULD_NOT_BREAK_REFERENCE_TO_ALIAS; import static org.cloudfoundry.identity.uaa.provider.IdpAliasFailedException.Reason.ORIGIN_KEY_ALREADY_USED_IN_ALIAS_ZONE; import static org.cloudfoundry.identity.uaa.util.UaaStringUtils.getCleanedUserControlString; import static org.springframework.http.HttpStatus.BAD_REQUEST; @@ -60,6 +61,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.dao.DataAccessException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -159,7 +161,7 @@ public ResponseEntity createIdentityProvider(@RequestBody Iden try { createdIdp = transactionTemplate.execute(txStatus -> { final IdentityProvider createdOriginalIdp = identityProviderProvisioning.create(body, zoneId); - return ensureConsistencyOfAliasIdp(createdOriginalIdp); + return ensureConsistencyOfAliasIdp(createdOriginalIdp, null); }); } catch (final IdpAlreadyExistsException e) { return new ResponseEntity<>(body, CONFLICT); @@ -266,7 +268,7 @@ public ResponseEntity updateIdentityProvider(@PathVariable Str try { updatedIdp = transactionTemplate.execute(txStatus -> { final IdentityProvider updatedOriginalIdp = identityProviderProvisioning.update(body, zoneId); - return ensureConsistencyOfAliasIdp(updatedOriginalIdp); + return ensureConsistencyOfAliasIdp(updatedOriginalIdp, existing); }); } catch (final IdpAliasFailedException e) { logger.warn("Could not create alias for {}", e.getMessage()); @@ -441,9 +443,13 @@ private boolean aliasPropertiesAreValid( * Ensure consistency during create or update operations with an alias IdP referenced in the original IdPs alias * properties. If the IdP has both its alias ID and alias ZID set, the existing alias IdP is updated. If only * the alias ZID is set, a new alias IdP is created. - * This method should be executed in a transaction together with the original create or update operation. + * This method should be executed in a transaction together with the original create or update operation. It is also + * assumed that {@link IdentityProviderEndpoints#aliasPropertiesAreValid} returned {@code true} for the combination + * of original IdP and existing IdP. * - * @param originalIdp the original IdP; must be persisted, i.e., have an ID, already + * @param originalIdp the original IdP; (changes to) it must already be persisted and its ID must therefore also be + * present already + * @param existingIdp the existing IdP before the update operation; for creation operations, this is {@code null} * @return the original IdP after the operation, with a potentially updated "aliasId" field * @throws IdpAliasFailedException if a new alias IdP needs to be created, but the zone referenced in 'aliasZid' * does not exist @@ -451,13 +457,62 @@ private boolean aliasPropertiesAreValid( * alias IdP could not be found */ private IdentityProvider ensureConsistencyOfAliasIdp( - final IdentityProvider originalIdp + @NonNull final IdentityProvider originalIdp, + @Nullable final IdentityProvider existingIdp ) throws IdpAliasFailedException { + /* If the IdP had an alias before the update and the alias feature is now turned off, we break the reference + * between the IdP and its alias by setting aliasId and aliasZid to null for both of them. Then, all other + * changes are only applied to the original IdP. */ + final boolean idpHadAlias = existingIdp != null && hasText(existingIdp.getAliasZid()); + final boolean referenceBreakRequired = idpHadAlias && !aliasEntitiesEnabled; + if (referenceBreakRequired) { + if (!hasText(existingIdp.getAliasId())) { + logger.warn( + "The state of the IdP [id={},zid={}] before the update had an aliasZid set, but no aliasId.", + existingIdp.getId(), + existingIdp.getIdentityZoneId() + ); + return originalIdp; + } + + final IdentityProvider aliasIdp = retrieveAliasIdp(existingIdp); + if (aliasIdp == null) { + logger.warn( + "The referenced alias IdP [id='{}',zid='{}'] does not exist, therefore cannot break reference.", + existingIdp.getAliasId(), + existingIdp.getAliasZid() + ); + return originalIdp; + } + + aliasIdp.setAliasId(null); + aliasIdp.setAliasZid(null); + + try { + identityProviderProvisioning.update(aliasIdp, aliasIdp.getIdentityZoneId()); + } catch (final DataAccessException e) { + throw new IdpAliasFailedException(existingIdp, COULD_NOT_BREAK_REFERENCE_TO_ALIAS, e); + } + + // no change required in the original IdP since its aliasId and aliasZid were already set to null + return originalIdp; + } + if (!hasText(originalIdp.getAliasZid())) { // no alias creation/update is necessary return originalIdp; } + if (!aliasEntitiesEnabled) { + /* Since we assume that the alias property validation was performed on the original IdP, both alias + * properties should be set to null whenever the alias feature is disabled. */ + throw new IllegalStateException(String.format( + "The IdP [id='%s',zid='%s'] has non-empty aliasZid, even though alias entities are disabled.", + originalIdp.getId(), + originalIdp.getIdentityZoneId() + )); + } + final IdentityProvider aliasIdp = new IdentityProvider<>(); aliasIdp.setActive(originalIdp.isActive()); aliasIdp.setName(originalIdp.getName()); diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java index 84a9d417985..d545be9689d 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java @@ -44,10 +44,13 @@ public enum Reason { "An IdP with this origin already exists in the alias zone.", HttpStatus.CONFLICT ), - ALIAS_ZONE_DOES_NOT_EXIST( "The referenced alias zone does not exist.", HttpStatus.UNPROCESSABLE_ENTITY + ), + COULD_NOT_BREAK_REFERENCE_TO_ALIAS( + "Could not break reference to alias IdP.", + HttpStatus.UNPROCESSABLE_ENTITY ); private final String message; From 9957813aefc26f0ca0d0fadff68e15b22165e284 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 25 Jan 2024 16:29:49 +0100 Subject: [PATCH 80/91] Add mock mvc tests for alias feature flag handling --- .../provider/IdentityProviderEndpoints.java | 1 + ...ityProviderEndpointsAliasMockMvcTests.java | 1045 +++++++++++------ 2 files changed, 701 insertions(+), 345 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 5e459579fcc..b4494e3c916 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 @@ -206,6 +206,7 @@ public ResponseEntity deleteIdentityProvider(@PathVariable Str if (hasText(existing.getAliasZid()) && hasText(existing.getAliasId())) { final IdentityProvider aliasIdp = retrieveAliasIdp(existing); if (aliasIdp == null) { + // ignore dangling reference to alias logger.warn( "Alias IdP referenced in IdentityProvider[origin={}; zone={}}] not found, skipping deletion of alias IdP.", existing.getOriginKey(), diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 634456c1097..69b638f24bf 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.stream.Stream; @@ -29,6 +30,7 @@ import org.cloudfoundry.identity.uaa.provider.AbstractExternalOAuthIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.IdentityProvider; +import org.cloudfoundry.identity.uaa.provider.IdentityProviderEndpoints; import org.cloudfoundry.identity.uaa.provider.JdbcIdentityProviderProvisioning; import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.PasswordPolicy; @@ -43,11 +45,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.ThrowingSupplier; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; @@ -76,6 +80,8 @@ class IdentityProviderEndpointsAliasMockMvcTests { private String adminToken; private String identityToken; + private IdentityProviderEndpoints identityProviderEndpoints; + @BeforeEach void setUp() throws Exception { adminToken = testClient.getClientCredentialsOAuthAccessToken( @@ -87,125 +93,211 @@ void setUp() throws Exception { "identitysecret", "zones.write"); customZone = MockMvcUtils.createZoneUsingWebRequest(mockMvc, identityToken); + + identityProviderEndpoints = Objects.requireNonNull(webApplicationContext.getBean(IdentityProviderEndpoints.class)); } @Nested class Create { - @Test - void shouldAccept_CreateAliasIdp_UaaToCustomZone() throws Exception { - shouldAccept_CreateAliasIdp(IdentityZone.getUaa(), customZone); - } + abstract class CreateBase { + private final boolean aliasFeatureEnabled; - @Test - void shouldAccept_CreateAliasIdp_CustomToUaaZone() throws Exception { - shouldAccept_CreateAliasIdp(customZone, IdentityZone.getUaa()); - } + protected CreateBase(final boolean aliasFeatureEnabled) { + this.aliasFeatureEnabled = aliasFeatureEnabled; + } - private void shouldAccept_CreateAliasIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - // build IdP in zone1 with aliasZid set to zone2 - final IdentityProvider provider = buildOidcIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); + @BeforeEach + void setUp() { + arrangeAliasFeatureEnabled(aliasFeatureEnabled); + } - // create IdP in zone1 - final IdentityProvider originalIdp = createIdp(zone1, provider); - assertThat(originalIdp).isNotNull(); - assertThat(originalIdp.getAliasId()).isNotBlank(); - assertThat(originalIdp.getAliasZid()).isNotBlank().isEqualTo(zone2.getId()); + @Test + void shouldAccept_AliasPropertiesNotSet_UaaZone() throws Exception { + shouldAccept_AliasPropertiesNotSet(IdentityZone.getUaa()); + } - // read alias IdP from zone2 - final String id = originalIdp.getAliasId(); - final Optional> aliasIdp = readIdpFromZoneIfExists(zone2.getId(), id); - assertThat(aliasIdp).isPresent(); - assertIdpReferencesOtherIdp(aliasIdp.get(), originalIdp); - assertOtherPropertiesAreEqual(originalIdp, aliasIdp.get()); + @Test + void shouldAccept_AliasPropertiesNotSet_CustomZone() throws Exception { + shouldAccept_AliasPropertiesNotSet(customZone); + } - // check if aliasId in first IdP is equal to the ID of the alias IdP - assertThat(aliasIdp.get().getId()).isEqualTo(originalIdp.getAliasId()); + private void shouldAccept_AliasPropertiesNotSet(final IdentityZone zone) throws Exception { + final IdentityProvider idp = buildOidcIdpWithAliasProperties( + zone.getId(), + null, + null + ); - // check if both have the same non-empty relying party secret - assertIdpAndAliasHaveSameRelyingPartySecretInDb(originalIdp); + final IdentityProvider createdIdp = createIdp(zone, idp); + assertThat(createdIdp).isNotNull(); + assertThat(createdIdp.getAliasId()).isBlank(); + assertThat(createdIdp.getAliasZid()).isBlank(); + } - // check if the returned IdP has a redacted relying party secret - assertRelyingPartySecretIsRedacted(originalIdp); - } + @Test + void shouldReject_AliasIdIsSet_UaaToCustomZone() throws Exception { + shouldReject_AliasIdIsSet(IdentityZone.getUaa(), customZone); + } - @Test - void shouldReject_IdzAndAliasZidAreEqual_UaaZone() throws Exception { - shouldReject_IdzAndAliasZidAreEqual(IdentityZone.getUaa()); - } + @Test + void shouldReject_AliasIdIsSet_CustomToUaaZone() throws Exception { + shouldReject_AliasIdIsSet(customZone, IdentityZone.getUaa()); + } - @Test - void shouldReject_IdzAndAliasZidAreEqual_CustomZone() throws Exception { - shouldReject_IdzAndAliasZidAreEqual(customZone); + private void shouldReject_AliasIdIsSet( + final IdentityZone zone1, + final IdentityZone zone2 + ) throws Exception { + final String aliasId = UUID.randomUUID().toString(); + final IdentityProvider idp = buildOidcIdpWithAliasProperties(zone1.getId(), aliasId, zone2.getId()); + shouldRejectCreation(zone1, idp, HttpStatus.UNPROCESSABLE_ENTITY); + } } - private void shouldReject_IdzAndAliasZidAreEqual(final IdentityZone zone) throws Exception { - final IdentityProvider idp = buildOidcIdpWithAliasProperties(zone.getId(), null, zone.getId()); - shouldRejectCreation(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); - } + @Nested + class AliasFeatureEnabled extends CreateBase { + protected AliasFeatureEnabled() { + super(true); + } - @Test - void shouldReject_AliasNotSupportedForIdpType_UaaToCustomZone() throws Exception { - shouldReject_AliasNotSupportedForIdpType(IdentityZone.getUaa(), customZone); - } + @Test + void shouldAccept_CreateAliasIdp_UaaToCustomZone() throws Exception { + shouldAccept_CreateAliasIdp(IdentityZone.getUaa(), customZone); + } - @Test - void shouldReject_AliasNotSupportedForIdpType_CustomToUaaZone() throws Exception { - shouldReject_AliasNotSupportedForIdpType(customZone, IdentityZone.getUaa()); - } + @Test + void shouldAccept_CreateAliasIdp_CustomToUaaZone() throws Exception { + shouldAccept_CreateAliasIdp(customZone, IdentityZone.getUaa()); + } - private void shouldReject_AliasNotSupportedForIdpType(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final IdentityProvider uaaIdp = buildUaaIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); - shouldRejectCreation(zone1, uaaIdp, HttpStatus.UNPROCESSABLE_ENTITY); - } + private void shouldAccept_CreateAliasIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + // build IdP in zone1 with aliasZid set to zone2 + final IdentityProvider provider = buildOidcIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); - @Test - void shouldReject_NeitherIdzNorAliasZidIsUaa() throws Exception { - final IdentityZone otherCustomZone = MockMvcUtils.createZoneUsingWebRequest(mockMvc, identityToken); - final IdentityProvider idp = buildOidcIdpWithAliasProperties(customZone.getId(), null, otherCustomZone.getId()); - shouldRejectCreation(customZone, idp, HttpStatus.UNPROCESSABLE_ENTITY); - } + // create IdP in zone1 + final IdentityProvider originalIdp = createIdp(zone1, provider); + assertThat(originalIdp).isNotNull(); + assertThat(originalIdp.getAliasId()).isNotBlank(); + assertThat(originalIdp.getAliasZid()).isNotBlank().isEqualTo(zone2.getId()); - @Test - void shouldReject_AliasIdIsSet() throws Exception { - final String aliasId = UUID.randomUUID().toString(); - final IdentityProvider idp = buildOidcIdpWithAliasProperties(customZone.getId(), aliasId, IdentityZone.getUaaZoneId()); - shouldRejectCreation(customZone, idp, HttpStatus.UNPROCESSABLE_ENTITY); - } + // read alias IdP from zone2 + final String id = originalIdp.getAliasId(); + final Optional> aliasIdp = readIdpFromZoneIfExists(zone2.getId(), id); + assertThat(aliasIdp).isPresent(); + assertIdpReferencesOtherIdp(aliasIdp.get(), originalIdp); + assertOtherPropertiesAreEqual(originalIdp, aliasIdp.get()); - @Test - void shouldReject_IdzReferencedInAliasZidDoesNotExist() throws Exception { - final IdentityProvider provider = buildOidcIdpWithAliasProperties( - IdentityZone.getUaaZoneId(), - null, - UUID.randomUUID().toString() // does not exist - ); - shouldRejectCreation(IdentityZone.getUaa(), provider, HttpStatus.UNPROCESSABLE_ENTITY); - } + // check if aliasId in first IdP is equal to the ID of the alias IdP + assertThat(aliasIdp.get().getId()).isEqualTo(originalIdp.getAliasId()); - @Test - void shouldReject_IdpWithOriginAlreadyExistsInAliasZone_CustomToUaaZone() throws Exception { - shouldReject_IdpWithOriginAlreadyExistsInAliasZone(customZone, IdentityZone.getUaa()); - } + // check if both have the same non-empty relying party secret + assertIdpAndAliasHaveSameRelyingPartySecretInDb(originalIdp); + + // check if the returned IdP has a redacted relying party secret + assertRelyingPartySecretIsRedacted(originalIdp); + } + + @Test + void shouldReject_IdzAndAliasZidAreEqual_UaaZone() throws Exception { + shouldReject_IdzAndAliasZidAreEqual(IdentityZone.getUaa()); + } + + @Test + void shouldReject_IdzAndAliasZidAreEqual_CustomZone() throws Exception { + shouldReject_IdzAndAliasZidAreEqual(customZone); + } + + private void shouldReject_IdzAndAliasZidAreEqual(final IdentityZone zone) throws Exception { + final IdentityProvider idp = buildOidcIdpWithAliasProperties(zone.getId(), null, zone.getId()); + shouldRejectCreation(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); + } + + @Test + void shouldReject_AliasNotSupportedForIdpType_UaaToCustomZone() throws Exception { + shouldReject_AliasNotSupportedForIdpType(IdentityZone.getUaa(), customZone); + } + + @Test + void shouldReject_AliasNotSupportedForIdpType_CustomToUaaZone() throws Exception { + shouldReject_AliasNotSupportedForIdpType(customZone, IdentityZone.getUaa()); + } + + private void shouldReject_AliasNotSupportedForIdpType(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + final IdentityProvider uaaIdp = buildUaaIdpWithAliasProperties(zone1.getId(), null, zone2.getId()); + shouldRejectCreation(zone1, uaaIdp, HttpStatus.UNPROCESSABLE_ENTITY); + } + + @Test + void shouldReject_NeitherIdzNorAliasZidIsUaa() throws Exception { + final IdentityZone otherCustomZone = MockMvcUtils.createZoneUsingWebRequest(mockMvc, identityToken); + final IdentityProvider idp = buildOidcIdpWithAliasProperties(customZone.getId(), null, otherCustomZone.getId()); + shouldRejectCreation(customZone, idp, HttpStatus.UNPROCESSABLE_ENTITY); + } + + @Test + void shouldReject_IdzReferencedInAliasZidDoesNotExist() throws Exception { + final IdentityProvider provider = buildOidcIdpWithAliasProperties( + IdentityZone.getUaaZoneId(), + null, + UUID.randomUUID().toString() // does not exist + ); + shouldRejectCreation(IdentityZone.getUaa(), provider, HttpStatus.UNPROCESSABLE_ENTITY); + } + + @Test + void shouldReject_IdpWithOriginAlreadyExistsInAliasZone_CustomToUaaZone() throws Exception { + shouldReject_IdpWithOriginAlreadyExistsInAliasZone(customZone, IdentityZone.getUaa()); + } + + @Test + void shouldReject_IdpWithOriginAlreadyExistsInAliasZone_UaaToCustomZone() throws Exception { + shouldReject_IdpWithOriginAlreadyExistsInAliasZone(IdentityZone.getUaa(), customZone); + } + + private void shouldReject_IdpWithOriginAlreadyExistsInAliasZone(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + // create IdP with origin key in zone 1 + final IdentityProvider createdIdp1 = createIdp( + zone1, + buildOidcIdpWithAliasProperties(zone1.getId(), null, null) + ); + assertThat(createdIdp1).isNotNull(); - @Test - void shouldReject_IdpWithOriginAlreadyExistsInAliasZone_UaaToCustomZone() throws Exception { - shouldReject_IdpWithOriginAlreadyExistsInAliasZone(IdentityZone.getUaa(), customZone); + // then, create an IdP in zone 2 with the same origin key for which an alias in zone 1 should be created -> should fail + shouldRejectCreation( + zone2, + buildIdpWithAliasProperties(zone2.getId(), null, zone1.getId(), createdIdp1.getOriginKey(), OIDC10), + HttpStatus.CONFLICT + ); + } } - private void shouldReject_IdpWithOriginAlreadyExistsInAliasZone(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - // create IdP with origin key in zone 1 - final IdentityProvider createdIdp1 = createIdp( - zone1, - buildOidcIdpWithAliasProperties(zone1.getId(), null, null) - ); - assertThat(createdIdp1).isNotNull(); + @Nested + class AliasFeatureDisabled extends CreateBase { + protected AliasFeatureDisabled() { + super(false); + } - // then, create an IdP in zone 2 with the same origin key for which an alias in zone 1 should be created -> should fail - shouldRejectCreation( - zone2, - buildIdpWithAliasProperties(zone2.getId(), null, zone1.getId(), createdIdp1.getOriginKey(), OIDC10), - HttpStatus.CONFLICT - ); + @Test + void shouldReject_OnlyAliasZidSet_UaaToCustomZone() throws Exception { + shouldReject_OnlyAliasZidSet(IdentityZone.getUaa(), customZone); + } + + @Test + void shouldReject_OnlyAliasZidSet_CustomToUaaZone() throws Exception { + shouldReject_OnlyAliasZidSet(customZone, IdentityZone.getUaa()); + } + + private void shouldReject_OnlyAliasZidSet( + final IdentityZone zone1, + final IdentityZone zone2 + ) throws Exception { + final IdentityProvider idp = buildOidcIdpWithAliasProperties( + zone1.getId(), + null, + zone2.getId() + ); + shouldRejectCreation(zone1, idp, HttpStatus.UNPROCESSABLE_ENTITY); + } } private void shouldRejectCreation(final IdentityZone zone, final IdentityProvider idp, final HttpStatus expectedStatus) throws Exception { @@ -223,243 +315,433 @@ private void shouldRejectCreation(final IdentityZone zone, final IdentityProvide @Nested class Update { - @Test - void shouldAccept_ShouldCreateAliasIdp_UaaToCustomZone() throws Exception { - shouldAccept_ShouldCreateAliasIdp(IdentityZone.getUaa(), customZone); - } + abstract class UpdateBase { + protected final boolean aliasFeatureEnabled; - @Test - void shouldAccept_ShouldCreateAliasIdp_CustomToUaaZone() throws Exception { - shouldAccept_ShouldCreateAliasIdp(customZone, IdentityZone.getUaa()); - } + protected UpdateBase(final boolean aliasFeatureEnabled) { + this.aliasFeatureEnabled = aliasFeatureEnabled; + } - private void shouldAccept_ShouldCreateAliasIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - // create regular idp without alias properties in zone 1 - final IdentityProvider existingIdpWithoutAlias = createIdp( - zone1, - buildOidcIdpWithAliasProperties(zone1.getId(), null, null) - ); - assertThat(existingIdpWithoutAlias).isNotNull(); - assertThat(existingIdpWithoutAlias.getId()).isNotBlank(); - - // perform update: set Alias ZID - existingIdpWithoutAlias.setAliasZid(zone2.getId()); - final IdentityProvider idpAfterUpdate = updateIdp(zone1, existingIdpWithoutAlias); - assertThat(idpAfterUpdate.getAliasId()).isNotBlank(); - assertThat(idpAfterUpdate.getAliasZid()).isNotBlank(); - assertThat(zone2.getId()).isEqualTo(idpAfterUpdate.getAliasZid()); - - // read alias IdP through alias id in original IdP - final String id = idpAfterUpdate.getAliasId(); - final Optional> idp = readIdpFromZoneIfExists(zone2.getId(), id); - assertThat(idp).isPresent(); - final IdentityProvider aliasIdp = idp.get(); - assertIdpReferencesOtherIdp(aliasIdp, idpAfterUpdate); - assertOtherPropertiesAreEqual(idpAfterUpdate, aliasIdp); + @BeforeEach + void setUp() { + arrangeAliasFeatureEnabled(aliasFeatureEnabled); + } } - @Test - void shouldAccept_OtherPropertiesOfIdpWithAliasAreChanged_UaaToCustomZone() throws Exception { - shouldAccept_OtherPropertiesOfIdpWithAliasAreChanged(IdentityZone.getUaa(), customZone); - } + @Nested + class AliasFeatureEnabled extends UpdateBase { + protected AliasFeatureEnabled() { + super(true); + } - @Test - void shouldAccept_OtherPropertiesOfIdpWithAliasAreChanged_CustomToUaaZone() throws Exception { - shouldAccept_OtherPropertiesOfIdpWithAliasAreChanged(customZone, IdentityZone.getUaa()); - } + @Test + void shouldAccept_ShouldCreateAliasIdp_UaaToCustomZone() throws Exception { + shouldAccept_ShouldCreateAliasIdp(IdentityZone.getUaa(), customZone); + } - private void shouldAccept_OtherPropertiesOfIdpWithAliasAreChanged(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - // create an IdP with an alias - final IdentityProvider originalIdp = createIdpWithAlias(zone1, zone2); - - // update other property - final String newName = "new name"; - originalIdp.setName(newName); - final IdentityProvider updatedOriginalIdp = updateIdp(zone1, originalIdp); - assertThat(updatedOriginalIdp).isNotNull(); - assertThat(updatedOriginalIdp.getAliasId()).isNotBlank(); - assertThat(updatedOriginalIdp.getAliasZid()).isNotBlank(); - assertThat(updatedOriginalIdp.getAliasZid()).isEqualTo(zone2.getId()); - assertThat(updatedOriginalIdp.getName()).isNotBlank().isEqualTo(newName); - - // check if the change is propagated to the alias IdP - final String id = updatedOriginalIdp.getAliasId(); - final Optional> aliasIdp = readIdpFromZoneIfExists(zone2.getId(), id); - assertThat(aliasIdp).isPresent(); - assertIdpReferencesOtherIdp(aliasIdp.get(), updatedOriginalIdp); - assertThat(aliasIdp.get().getName()).isNotBlank().isEqualTo(newName); - - // check if both have the same non-empty relying party secret in the DB - assertIdpAndAliasHaveSameRelyingPartySecretInDb(updatedOriginalIdp); - - // check if the returned IdP has a redacted relying party secret - assertRelyingPartySecretIsRedacted(updatedOriginalIdp); - } + @Test + void shouldAccept_ShouldCreateAliasIdp_CustomToUaaZone() throws Exception { + shouldAccept_ShouldCreateAliasIdp(customZone, IdentityZone.getUaa()); + } - @Test - void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewAliasIdp_UaaToCustomZone() throws Exception { - shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewAliasIdp(IdentityZone.getUaa(), customZone); - } + private void shouldAccept_ShouldCreateAliasIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + // create regular idp without alias properties in zone 1 + final IdentityProvider existingIdpWithoutAlias = createIdp( + zone1, + buildOidcIdpWithAliasProperties(zone1.getId(), null, null) + ); + assertThat(existingIdpWithoutAlias).isNotNull(); + assertThat(existingIdpWithoutAlias.getId()).isNotBlank(); + + // perform update: set Alias ZID + existingIdpWithoutAlias.setAliasZid(zone2.getId()); + final IdentityProvider idpAfterUpdate = updateIdp(zone1, existingIdpWithoutAlias); + assertThat(idpAfterUpdate.getAliasId()).isNotBlank(); + assertThat(idpAfterUpdate.getAliasZid()).isNotBlank(); + assertThat(zone2.getId()).isEqualTo(idpAfterUpdate.getAliasZid()); + + // read alias IdP through alias id in original IdP + final String id = idpAfterUpdate.getAliasId(); + final Optional> idp = readIdpFromZoneIfExists(zone2.getId(), id); + assertThat(idp).isPresent(); + final IdentityProvider aliasIdp = idp.get(); + assertIdpReferencesOtherIdp(aliasIdp, idpAfterUpdate); + assertOtherPropertiesAreEqual(idpAfterUpdate, aliasIdp); + } - @Test - void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewAliasIdp_CustomToUaaZone() throws Exception { - shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewAliasIdp(customZone, IdentityZone.getUaa()); - } + @Test + void shouldAccept_OtherPropertiesOfIdpWithAliasAreChanged_UaaToCustomZone() throws Exception { + shouldAccept_OtherPropertiesOfIdpWithAliasAreChanged(IdentityZone.getUaa(), customZone); + } - private void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewAliasIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final IdentityProvider idp = createIdpWithAlias(zone1, zone2); + @Test + void shouldAccept_OtherPropertiesOfIdpWithAliasAreChanged_CustomToUaaZone() throws Exception { + shouldAccept_OtherPropertiesOfIdpWithAliasAreChanged(customZone, IdentityZone.getUaa()); + } - // delete the alias IdP directly in the DB -> after that, there is a dangling reference - deleteIdpViaDb(idp.getOriginKey(), zone2.getId()); + private void shouldAccept_OtherPropertiesOfIdpWithAliasAreChanged(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + // create an IdP with an alias + final IdentityProvider originalIdp = createIdpWithAlias(zone1, zone2); + + // update other property + final String newName = "new name"; + originalIdp.setName(newName); + final IdentityProvider updatedOriginalIdp = updateIdp(zone1, originalIdp); + assertThat(updatedOriginalIdp).isNotNull(); + assertThat(updatedOriginalIdp.getAliasId()).isNotBlank(); + assertThat(updatedOriginalIdp.getAliasZid()).isNotBlank(); + assertThat(updatedOriginalIdp.getAliasZid()).isEqualTo(zone2.getId()); + assertThat(updatedOriginalIdp.getName()).isNotBlank().isEqualTo(newName); + + // check if the change is propagated to the alias IdP + final String id = updatedOriginalIdp.getAliasId(); + final Optional> aliasIdp = readIdpFromZoneIfExists(zone2.getId(), id); + assertThat(aliasIdp).isPresent(); + assertIdpReferencesOtherIdp(aliasIdp.get(), updatedOriginalIdp); + assertThat(aliasIdp.get().getName()).isNotBlank().isEqualTo(newName); + + // check if both have the same non-empty relying party secret in the DB + assertIdpAndAliasHaveSameRelyingPartySecretInDb(updatedOriginalIdp); + + // check if the returned IdP has a redacted relying party secret + assertRelyingPartySecretIsRedacted(updatedOriginalIdp); + } - // update some other property on the original IdP - idp.setName("some-new-name"); - final IdentityProvider updatedIdp = updateIdp(zone1, idp); - assertThat(updatedIdp.getAliasId()).isNotBlank().isNotEqualTo(idp.getAliasId()); - assertThat(updatedIdp.getAliasZid()).isNotBlank().isEqualTo(idp.getAliasZid()); + @Test + void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewAliasIdp_UaaToCustomZone() throws Exception { + shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewAliasIdp(IdentityZone.getUaa(), customZone); + } - // check if the new alias IdP is present and has the correct properties - final String id = updatedIdp.getAliasId(); - final Optional> aliasIdp = readIdpFromZoneIfExists(zone2.getId(), id); - assertThat(aliasIdp).isPresent(); - assertIdpReferencesOtherIdp(updatedIdp, aliasIdp.get()); - assertOtherPropertiesAreEqual(updatedIdp, aliasIdp.get()); + @Test + void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewAliasIdp_CustomToUaaZone() throws Exception { + shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewAliasIdp(customZone, IdentityZone.getUaa()); + } - // check if both have the same non-empty relying party secret - assertIdpAndAliasHaveSameRelyingPartySecretInDb(updatedIdp); + private void shouldAccept_ReferencedIdpNotExisting_ShouldCreateNewAliasIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + final IdentityProvider idp = createIdpWithAlias(zone1, zone2); - // check if the returned IdP has a redacted relying party secret - assertRelyingPartySecretIsRedacted(updatedIdp); - } + // delete the alias IdP directly in the DB -> after that, there is a dangling reference + deleteIdpViaDb(idp.getOriginKey(), zone2.getId()); - @ParameterizedTest - @MethodSource("shouldReject_ChangingAliasPropertiesOfIdpWithAlias") - void shouldReject_ChangingAliasPropertiesOfIdpWithAlias_UaaToCustomZone(final String newAliasId, final String newAliasZid) throws Exception { - shouldReject_ChangingAliasPropertiesOfIdpWithAlias(newAliasId, newAliasZid, IdentityZone.getUaa(), customZone); - } + // update some other property on the original IdP + idp.setName("some-new-name"); + final IdentityProvider updatedIdp = updateIdp(zone1, idp); + assertThat(updatedIdp.getAliasId()).isNotBlank().isNotEqualTo(idp.getAliasId()); + assertThat(updatedIdp.getAliasZid()).isNotBlank().isEqualTo(idp.getAliasZid()); - @ParameterizedTest - @MethodSource("shouldReject_ChangingAliasPropertiesOfIdpWithAlias") - void shouldReject_ChangingAliasPropertiesOfIdpWithAlias_CustomToUaaZone(final String newAliasId, final String newAliasZid) throws Exception { - shouldReject_ChangingAliasPropertiesOfIdpWithAlias(newAliasId, newAliasZid, customZone, IdentityZone.getUaa()); - } + // check if the new alias IdP is present and has the correct properties + final String id = updatedIdp.getAliasId(); + final Optional> aliasIdp = readIdpFromZoneIfExists(zone2.getId(), id); + assertThat(aliasIdp).isPresent(); + assertIdpReferencesOtherIdp(updatedIdp, aliasIdp.get()); + assertOtherPropertiesAreEqual(updatedIdp, aliasIdp.get()); - private void shouldReject_ChangingAliasPropertiesOfIdpWithAlias( - final String newAliasId, - final String newAliasZid, - final IdentityZone zone1, - final IdentityZone zone2 - ) throws Exception { - final IdentityProvider originalIdp = createIdpWithAlias(zone1, zone2); - originalIdp.setAliasId(newAliasId); - originalIdp.setAliasZid(newAliasZid); - shouldRejectUpdate(zone1, originalIdp, HttpStatus.UNPROCESSABLE_ENTITY); - } + // check if both have the same non-empty relying party secret + assertIdpAndAliasHaveSameRelyingPartySecretInDb(updatedIdp); - private static Stream shouldReject_ChangingAliasPropertiesOfIdpWithAlias() { - return Stream.of(null, "", "other").flatMap(aliasIdValue -> - Stream.of(null, "", "other").map(aliasZidValue -> - Arguments.of(aliasIdValue, aliasZidValue) - )); - } + // check if the returned IdP has a redacted relying party secret + assertRelyingPartySecretIsRedacted(updatedIdp); + } - @Test - void shouldReject_OnlyAliasIdSet_UaaZone() throws Exception { - shouldReject_OnlyAliasIdSet(IdentityZone.getUaa()); - } + @Test + void shouldReject_OnlyAliasIdSet_UaaZone() throws Exception { + shouldReject_OnlyAliasIdSet(IdentityZone.getUaa()); + } - @Test - void shouldReject_OnlyAliasIdSet_CustomZone() throws Exception { - shouldReject_OnlyAliasIdSet(customZone); - } + @Test + void shouldReject_OnlyAliasIdSet_CustomZone() throws Exception { + shouldReject_OnlyAliasIdSet(customZone); + } - private void shouldReject_OnlyAliasIdSet(final IdentityZone zone) throws Exception { - final IdentityProvider idp = buildOidcIdpWithAliasProperties(zone.getId(), null, null); - final IdentityProvider createdProvider = createIdp(zone, idp); - assertThat(createdProvider.getAliasZid()).isBlank(); - createdProvider.setAliasId(UUID.randomUUID().toString()); - shouldRejectUpdate(zone, createdProvider, HttpStatus.UNPROCESSABLE_ENTITY); - } + private void shouldReject_OnlyAliasIdSet(final IdentityZone zone) throws Exception { + final IdentityProvider idp = buildOidcIdpWithAliasProperties(zone.getId(), null, null); + final IdentityProvider createdProvider = createIdp(zone, idp); + assertThat(createdProvider.getAliasZid()).isBlank(); + createdProvider.setAliasId(UUID.randomUUID().toString()); + shouldRejectUpdate(zone, createdProvider, HttpStatus.UNPROCESSABLE_ENTITY); + } - @Test - void shouldReject_AliasNotSupportedForIdpType_UaaToCustomZone() throws Exception { - shouldReject_AliasNotSupportedForIdpType(IdentityZone.getUaa(), customZone); - } + @ParameterizedTest + @MethodSource("shouldReject_ChangingAliasPropertiesOfIdpWithAlias") + void shouldReject_ChangingAliasPropertiesOfIdpWithAlias_UaaToCustomZone( + final String newAliasId, + final String newAliasZid + ) throws Throwable { + shouldReject_ChangingAliasPropertiesOfIdpWithAlias(newAliasId, newAliasZid, IdentityZone.getUaa(), customZone); + } - @Test - void shouldReject_AliasNotSupportedForIdpType_CustomZone() throws Exception { - shouldReject_AliasNotSupportedForIdpType(customZone, IdentityZone.getUaa()); - } + @ParameterizedTest + @MethodSource("shouldReject_ChangingAliasPropertiesOfIdpWithAlias") + void shouldReject_ChangingAliasPropertiesOfIdpWithAlias_CustomToUaaZone( + final String newAliasId, + final String newAliasZid + ) throws Throwable { + shouldReject_ChangingAliasPropertiesOfIdpWithAlias(newAliasId, newAliasZid, customZone, IdentityZone.getUaa()); + } - private void shouldReject_AliasNotSupportedForIdpType(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final IdentityProvider uaaIdp = buildUaaIdpWithAliasProperties(zone1.getId(), null, null); - final IdentityProvider createdProvider = createIdp(zone1, uaaIdp); - assertThat(createdProvider.getAliasZid()).isBlank(); + private void shouldReject_ChangingAliasPropertiesOfIdpWithAlias( + final String newAliasId, + final String newAliasZid, + final IdentityZone zone1, + final IdentityZone zone2 + ) throws Throwable { + final IdentityProvider originalIdp = executeWithTemporarilyEnabledAliasFeature( + aliasFeatureEnabled, + () -> createIdpWithAlias(zone1, zone2) + ); + originalIdp.setAliasId(newAliasId); + originalIdp.setAliasZid(newAliasZid); + shouldRejectUpdate(zone1, originalIdp, HttpStatus.UNPROCESSABLE_ENTITY); + } - // try to create an alias for the IdP -> should fail because of the IdP's type - createdProvider.setAliasZid(zone2.getId()); - shouldRejectUpdate(zone1, createdProvider, HttpStatus.UNPROCESSABLE_ENTITY); - } + private static Stream shouldReject_ChangingAliasPropertiesOfIdpWithAlias() { + return Stream.of(null, "", "other").flatMap(aliasIdValue -> + Stream.of(null, "", "other").map(aliasZidValue -> + Arguments.of(aliasIdValue, aliasZidValue) + )); + } - @Test - void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone_UaaToCustomZone() throws Exception { - shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(IdentityZone.getUaa(), customZone); - } + @Test + void shouldReject_AliasNotSupportedForIdpType_UaaToCustomZone() throws Exception { + shouldReject_AliasNotSupportedForIdpType(IdentityZone.getUaa(), customZone); + } - @Test - void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone_CustomToUaaZone() throws Exception { - shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(customZone, IdentityZone.getUaa()); - } + @Test + void shouldReject_AliasNotSupportedForIdpType_CustomZone() throws Exception { + shouldReject_AliasNotSupportedForIdpType(customZone, IdentityZone.getUaa()); + } - private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - // create IdP with origin key in zone 2 - final IdentityProvider existingIdpInZone2 = buildOidcIdpWithAliasProperties(zone2.getId(), null, null); - createIdp(zone2, existingIdpInZone2); - - // create IdP with same origin key in zone 1 - final IdentityProvider idp = buildIdpWithAliasProperties( - zone1.getId(), - null, - null, - existingIdpInZone2.getOriginKey(), // same origin key - OIDC10 - ); - final IdentityProvider providerInZone1 = createIdp(zone1, idp); + private void shouldReject_AliasNotSupportedForIdpType(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + final IdentityProvider uaaIdp = buildUaaIdpWithAliasProperties(zone1.getId(), null, null); + final IdentityProvider createdProvider = createIdp(zone1, uaaIdp); + assertThat(createdProvider.getAliasZid()).isBlank(); - // update the alias ZID to zone 2, where an IdP with this origin already exists -> should fail - providerInZone1.setAliasZid(zone2.getId()); - shouldRejectUpdate(zone1, providerInZone1, HttpStatus.CONFLICT); - } + // try to create an alias for the IdP -> should fail because of the IdP's type + createdProvider.setAliasZid(zone2.getId()); + shouldRejectUpdate(zone1, createdProvider, HttpStatus.UNPROCESSABLE_ENTITY); + } - @Test - void shouldReject_IdpInCustomZone_AliasToOtherCustomZone() throws Exception { - final IdentityProvider idpInCustomZone = createIdp( - customZone, - buildOidcIdpWithAliasProperties(customZone.getId(), null, null) - ); + @Test + void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone_UaaToCustomZone() throws Exception { + shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(IdentityZone.getUaa(), customZone); + } - // try to create an alias in another custom zone -> should fail - idpInCustomZone.setAliasZid("not-uaa"); - shouldRejectUpdate(customZone, idpInCustomZone, HttpStatus.UNPROCESSABLE_ENTITY); - } + @Test + void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone_CustomToUaaZone() throws Exception { + shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(customZone, IdentityZone.getUaa()); + } - @Test - void shouldReject_AliasZidSetToSameZone_UaaZone() throws Exception { - shouldReject_AliasZidSetToSameZone(IdentityZone.getUaa()); - } + private void shouldReject_IdpWithOriginKeyAlreadyPresentInOtherZone(final IdentityZone zone1, final IdentityZone zone2) throws Exception { + // create IdP with origin key in zone 2 + final IdentityProvider existingIdpInZone2 = buildOidcIdpWithAliasProperties(zone2.getId(), null, null); + createIdp(zone2, existingIdpInZone2); + + // create IdP with same origin key in zone 1 + final IdentityProvider idp = buildIdpWithAliasProperties( + zone1.getId(), + null, + null, + existingIdpInZone2.getOriginKey(), // same origin key + OIDC10 + ); + final IdentityProvider providerInZone1 = createIdp(zone1, idp); + + // update the alias ZID to zone 2, where an IdP with this origin already exists -> should fail + providerInZone1.setAliasZid(zone2.getId()); + shouldRejectUpdate(zone1, providerInZone1, HttpStatus.CONFLICT); + } + + @Test + void shouldReject_IdpInCustomZone_AliasToOtherCustomZone() throws Exception { + final IdentityProvider idpInCustomZone = createIdp( + customZone, + buildOidcIdpWithAliasProperties(customZone.getId(), null, null) + ); + + // try to create an alias in another custom zone -> should fail + idpInCustomZone.setAliasZid("not-uaa"); + shouldRejectUpdate(customZone, idpInCustomZone, HttpStatus.UNPROCESSABLE_ENTITY); + } + + @Test + void shouldReject_AliasZidSetToSameZone_UaaZone() throws Exception { + shouldReject_AliasZidSetToSameZone(IdentityZone.getUaa()); + } - @Test - void shouldReject_AliasZidSetToSameZone_CustomZone() throws Exception { - shouldReject_AliasZidSetToSameZone(customZone); + @Test + void shouldReject_AliasZidSetToSameZone_CustomZone() throws Exception { + shouldReject_AliasZidSetToSameZone(customZone); + } + + private void shouldReject_AliasZidSetToSameZone(final IdentityZone zone) throws Exception { + final IdentityProvider idp = createIdp( + zone, + buildOidcIdpWithAliasProperties(zone.getId(), null, null) + ); + idp.setAliasZid(zone.getId()); + shouldRejectUpdate(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); + } } - private void shouldReject_AliasZidSetToSameZone(final IdentityZone zone) throws Exception { - final IdentityProvider idp = createIdp( - zone, - buildOidcIdpWithAliasProperties(zone.getId(), null, null) - ); - idp.setAliasZid(zone.getId()); - shouldRejectUpdate(zone, idp, HttpStatus.UNPROCESSABLE_ENTITY); + @Nested + class AliasFeatureDisabled extends UpdateBase { + protected AliasFeatureDisabled() { + super(false); + } + + @Test + void shouldReject_OtherPropertiesChangedWhileAliasPropertiesUnchanged_UaaToCustomZone() throws Throwable { + shouldReject_OtherPropertiesChangedWhileAliasPropertiesUnchanged(IdentityZone.getUaa(), customZone); + } + + @Test + void shouldReject_OtherPropertiesChangedWhileAliasPropertiesUnchanged_CustomToUaaZone() throws Throwable { + shouldReject_OtherPropertiesChangedWhileAliasPropertiesUnchanged(customZone, IdentityZone.getUaa()); + } + + private void shouldReject_OtherPropertiesChangedWhileAliasPropertiesUnchanged( + final IdentityZone zone1, + final IdentityZone zone2 + ) throws Throwable { + final IdentityProvider originalIdp = executeWithTemporarilyEnabledAliasFeature( + aliasFeatureEnabled, + () -> createIdpWithAlias(zone1, zone2) + ); + + // change non-alias property without setting alias properties to null + originalIdp.setName("some-new-name"); + shouldRejectUpdate(zone1, originalIdp, HttpStatus.UNPROCESSABLE_ENTITY); + } + + @Test + void shouldAccept_SetOnlyAliasPropertiesToNull_UaaToCustomZone() throws Throwable { + shouldAccept_SetOnlyAliasPropertiesToNull(IdentityZone.getUaa(), customZone); + } + + @Test + void shouldAccept_SetOnlyAliasPropertiesToNull_CustomToUaaZone() throws Throwable { + shouldAccept_SetOnlyAliasPropertiesToNull(customZone, IdentityZone.getUaa()); + } + + private void shouldAccept_SetOnlyAliasPropertiesToNull( + final IdentityZone zone1, + final IdentityZone zone2 + ) throws Throwable { + final IdentityProvider originalIdp = executeWithTemporarilyEnabledAliasFeature( + aliasFeatureEnabled, + () -> createIdpWithAlias(zone1, zone2) + ); + + final String initialAliasId = originalIdp.getAliasId(); + assertThat(initialAliasId).isNotBlank(); + final String initialAliasZid = originalIdp.getAliasZid(); + assertThat(initialAliasZid).isNotBlank(); + + // change non-alias property without setting alias properties to null + originalIdp.setAliasId(null); + originalIdp.setAliasZid(null); + final IdentityProvider updatedIdp = updateIdp(zone1, originalIdp); + assertThat(updatedIdp.getAliasId()).isBlank(); + assertThat(updatedIdp.getAliasZid()).isBlank(); + + // the alias IdP should have its reference removed + assertReferenceWasRemovedFromAlias(initialAliasId, initialAliasZid); + } + + @Test + void shouldAccept_SetAliasPropertiesToNullAndChangeOtherProperties_UaaToCustomZone() throws Throwable { + shouldAccept_SetAliasPropertiesToNullAndChangeOtherProperties(IdentityZone.getUaa(), customZone); + } + + @Test + void shouldAccept_SetAliasPropertiesToNullAndChangeOtherProperties_CustomToUaaZone() throws Throwable { + shouldAccept_SetAliasPropertiesToNullAndChangeOtherProperties(customZone, IdentityZone.getUaa()); + } + + private void shouldAccept_SetAliasPropertiesToNullAndChangeOtherProperties( + final IdentityZone zone1, + final IdentityZone zone2 + ) throws Throwable { + final IdentityProvider originalIdp = executeWithTemporarilyEnabledAliasFeature( + aliasFeatureEnabled, + () -> createIdpWithAlias(zone1, zone2) + ); + + final String initialAliasId = originalIdp.getAliasId(); + assertThat(initialAliasId).isNotBlank(); + final String initialAliasZid = originalIdp.getAliasZid(); + assertThat(initialAliasZid).isNotBlank(); + final String initialName = originalIdp.getName(); + assertThat(initialName).isNotBlank(); + + // change non-alias property without setting alias properties to null + originalIdp.setAliasId(null); + originalIdp.setAliasZid(null); + originalIdp.setName("some-new-name"); + final IdentityProvider updatedIdp = updateIdp(zone1, originalIdp); + assertThat(updatedIdp.getAliasId()).isBlank(); + assertThat(updatedIdp.getAliasZid()).isBlank(); + assertThat(updatedIdp.getName()).isEqualTo("some-new-name"); + + // apart from the alias reference being removed, the alias IdP should be left unchanged + final Optional> aliasIdpAfterUpdate = readIdpFromZoneIfExists(zone2.getId(), initialAliasId); + assertThat(aliasIdpAfterUpdate).isPresent(); + assertThat(aliasIdpAfterUpdate.get().getAliasId()).isBlank(); + assertThat(aliasIdpAfterUpdate.get().getAliasZid()).isBlank(); + assertThat(aliasIdpAfterUpdate.get().getName()).isEqualTo(initialName); + } + + @Test + void shouldReject_OnlyAliasIdSetToNull_UaaToCustomZone() throws Throwable { + shouldReject_OnlyAliasIdSetToNull(IdentityZone.getUaa(), customZone); + } + + @Test + void shouldReject_OnlyAliasIdSetToNull_CustomToUaaZone() throws Throwable { + shouldReject_OnlyAliasIdSetToNull(customZone, IdentityZone.getUaa()); + } + + private void shouldReject_OnlyAliasIdSetToNull( + final IdentityZone zone1, + final IdentityZone zone2 + ) throws Throwable { + final IdentityProvider originalIdp = executeWithTemporarilyEnabledAliasFeature( + aliasFeatureEnabled, + () -> createIdpWithAlias(zone1, zone2) + ); + + assertThat(originalIdp.getAliasId()).isNotBlank(); + assertThat(originalIdp.getAliasZid()).isNotBlank(); + + originalIdp.setAliasId(null); + shouldRejectUpdate(zone1, originalIdp, HttpStatus.UNPROCESSABLE_ENTITY); + } + + @Test + void shouldReject_OnlyAliasZidSetToNull_UaaToCustomZone() throws Throwable { + shouldReject_OnlyAliasZidSetToNull(IdentityZone.getUaa(), customZone); + } + + @Test + void shouldReject_OnlyAliasZidSetToNull_CustomToUaaZone() throws Throwable { + shouldReject_OnlyAliasZidSetToNull(customZone, IdentityZone.getUaa()); + } + + private void shouldReject_OnlyAliasZidSetToNull( + final IdentityZone zone1, + final IdentityZone zone2 + ) throws Throwable { + final IdentityProvider originalIdp = executeWithTemporarilyEnabledAliasFeature( + aliasFeatureEnabled, + () -> createIdpWithAlias(zone1, zone2) + ); + + assertThat(originalIdp.getAliasId()).isNotBlank(); + assertThat(originalIdp.getAliasZid()).isNotBlank(); + + originalIdp.setAliasZid(null); + shouldRejectUpdate(zone1, originalIdp, HttpStatus.UNPROCESSABLE_ENTITY); + } } private IdentityProvider updateIdp(final IdentityZone zone, final IdentityProvider updatePayload) throws Exception { @@ -536,64 +818,115 @@ private void shouldRejectUpdate(final IdentityZone zone, final IdentityProvider< @Nested class Delete { + abstract class DeleteBase { + protected final boolean aliasFeatureEnabled; - @Test - void shouldAlsoDeleteAliasIdp_UaaToCustomZone() throws Exception { - shouldAlsoDeleteAliasIdp(IdentityZone.getUaa(), customZone); - } + public DeleteBase(final boolean aliasFeatureEnabled) { + this.aliasFeatureEnabled = aliasFeatureEnabled; + } - @Test - void shouldAlsoDeleteAliasIdp_CustomToUaaZone() throws Exception { - shouldAlsoDeleteAliasIdp(customZone, IdentityZone.getUaa()); - } + @BeforeEach + void setUp() { + arrangeAliasFeatureEnabled(aliasFeatureEnabled); + } - private void shouldAlsoDeleteAliasIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final IdentityProvider idpInZone1 = createIdpWithAlias(zone1, zone2); - final String id = idpInZone1.getId(); - assertThat(id).isNotBlank(); - final String aliasId = idpInZone1.getAliasId(); - assertThat(aliasId).isNotBlank(); - final String aliasZid = idpInZone1.getAliasZid(); - assertThat(aliasZid).isNotBlank().isEqualTo(zone2.getId()); - - // check if alias IdP is available in zone 2 - final Optional> aliasIdp = readIdpFromZoneIfExists(zone2.getId(), aliasId); - assertThat(aliasIdp).isPresent(); - assertThat(aliasIdp.get().getAliasId()).isNotBlank().isEqualTo(id); - assertThat(aliasIdp.get().getAliasZid()).isNotBlank().isEqualTo(idpInZone1.getIdentityZoneId()); - - // delete IdP in zone 1 - final MvcResult deleteResult = deleteIdpAndReturnResult(zone1, id); - assertThat(deleteResult.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); - - // check if IdP is no longer available in zone 2 - assertIdpDoesNotExist(zone2, aliasId); - } + @Test + void shouldIgnoreDanglingReferenceToAliasIdp_UaaToCustomZone() throws Throwable { + shouldIgnoreDanglingReferenceToAliasIdp(IdentityZone.getUaa(), customZone); + } - @Test - void shouldIgnoreDanglingReferenceToAliasIdp_UaaToCustomZone() throws Exception { - shouldIgnoreDanglingReferenceToAliasIdp(IdentityZone.getUaa(), customZone); - } + @Test + void shouldIgnoreDanglingReferenceToAliasIdp_CustomToUaaZone() throws Throwable { + shouldIgnoreDanglingReferenceToAliasIdp(customZone, IdentityZone.getUaa()); + } - @Test - void shouldIgnoreDanglingReferenceToAliasIdp_CustomToUaaZone() throws Exception { - shouldIgnoreDanglingReferenceToAliasIdp(customZone, IdentityZone.getUaa()); + private void shouldIgnoreDanglingReferenceToAliasIdp(final IdentityZone zone1, final IdentityZone zone2) throws Throwable { + final IdentityProvider originalIdp = executeWithTemporarilyEnabledAliasFeature( + aliasFeatureEnabled, + () -> createIdpWithAlias(zone1, zone2) + ); + + // create a dangling reference by deleting the alias IdP directly in the DB + deleteIdpViaDb(originalIdp.getOriginKey(), zone2.getId()); + + // delete the original IdP -> dangling reference should be ignored + final MvcResult deleteResult = deleteIdpAndReturnResult(zone1, originalIdp.getId()); + assertThat(deleteResult.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); + + // original IdP should no longer exist + assertIdpDoesNotExist(originalIdp.getId(), zone1.getId()); + } + + @Test + void deletionWithExistingAliasIdp_UaaToCustomZone() throws Throwable { + deletionWithExistingAliasIdp(IdentityZone.getUaa(), customZone); + } + + @Test + void deletionWithExistingAliasIdp_CustomToUaaZone() throws Throwable { + deletionWithExistingAliasIdp(customZone, IdentityZone.getUaa()); + } + + private void deletionWithExistingAliasIdp( + final IdentityZone zone1, + final IdentityZone zone2 + ) throws Throwable { + // create IdP in zone 1 with alias in zone 2 + final IdentityProvider idpInZone1 = executeWithTemporarilyEnabledAliasFeature( + aliasFeatureEnabled, + () -> createIdpWithAlias(zone1, zone2) + ); + final String id = idpInZone1.getId(); + assertThat(id).isNotBlank(); + final String aliasId = idpInZone1.getAliasId(); + assertThat(aliasId).isNotBlank(); + final String aliasZid = idpInZone1.getAliasZid(); + assertThat(aliasZid).isNotBlank().isEqualTo(zone2.getId()); + + // check if alias IdP is available in zone 2 + final Optional> aliasIdp = readIdpFromZoneIfExists(zone2.getId(), aliasId); + assertThat(aliasIdp).isPresent(); + assertThat(aliasIdp.get().getAliasId()).isNotBlank().isEqualTo(id); + assertThat(aliasIdp.get().getAliasZid()).isNotBlank().isEqualTo(idpInZone1.getIdentityZoneId()); + + // delete IdP in zone 1 + final MvcResult deleteResult = deleteIdpAndReturnResult(zone1, id); + assertThat(deleteResult.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); + + // alias IdP should still exist, but without reference to original IdP + assertAliasIdpAfterDeletion(aliasId, aliasZid); + } + + protected abstract void assertAliasIdpAfterDeletion(final String aliasId, final String aliasZid) throws Exception; } - private void shouldIgnoreDanglingReferenceToAliasIdp(final IdentityZone zone1, final IdentityZone zone2) throws Exception { - final IdentityProvider originalIdp = createIdpWithAlias(zone1, zone2); + @Nested + class AliasFeatureEnabled extends DeleteBase { + public AliasFeatureEnabled() { + super(true); + } - // create a dangling reference by deleting the alias IdP directly in the DB - deleteIdpViaDb(originalIdp.getOriginKey(), zone2.getId()); + @Override + protected void assertAliasIdpAfterDeletion(final String aliasId, final String aliasZid) throws Exception { + // if the alias feature is enabled, the alias should also be removed + assertIdpDoesNotExist(aliasId, aliasZid); + } + } - // delete the original IdP -> dangling reference should be ignored - final MvcResult deleteResult = deleteIdpAndReturnResult(zone1, originalIdp.getId()); - assertThat(deleteResult.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); + @Nested + class AliasFeatureDisabled extends DeleteBase { + public AliasFeatureDisabled() { + super(false); + } - // original IdP should no longer exist - assertIdpDoesNotExist(zone1, originalIdp.getId()); + @Override + protected void assertAliasIdpAfterDeletion(final String aliasId, final String aliasZid) throws Exception { + // if the alias feature is disabled, only the reference should be removed from the alias IdP + assertReferenceWasRemovedFromAlias(aliasId, aliasZid); + } } + private MvcResult deleteIdpAndReturnResult(final IdentityZone zone, final String id) throws Exception { final String accessTokenForZone1 = getAccessTokenForZone(zone.getId()); final MockHttpServletRequestBuilder deleteRequestBuilder = delete("/identity-providers/" + id) @@ -602,11 +935,17 @@ private MvcResult deleteIdpAndReturnResult(final IdentityZone zone, final String return mockMvc.perform(deleteRequestBuilder).andReturn(); } - private void assertIdpDoesNotExist(final IdentityZone zone, final String id) throws Exception { - final Optional> idp = readIdpFromZoneIfExists(zone.getId(), id); + private void assertIdpDoesNotExist(final String id, final String zoneId) throws Exception { + final Optional> idp = readIdpFromZoneIfExists(zoneId, id); assertThat(idp).isNotPresent(); } + } + private void assertReferenceWasRemovedFromAlias(final String aliasId, final String aliasZid) throws Exception { + final Optional> aliasIdpAfterDeletion = readIdpFromZoneIfExists(aliasZid, aliasId); + assertThat(aliasIdpAfterDeletion).isPresent(); + assertThat(aliasIdpAfterDeletion.get().getAliasId()).isBlank(); + assertThat(aliasIdpAfterDeletion.get().getAliasZid()).isBlank(); } private void assertIdpReferencesOtherIdp(final IdentityProvider idp, final IdentityProvider referencedIdp) { @@ -652,6 +991,18 @@ private MvcResult createIdpAndReturnResult(final IdentityZone zone, final Identi return mockMvc.perform(createRequestBuilder).andReturn(); } + private T executeWithTemporarilyEnabledAliasFeature( + final boolean aliasFeatureEnabledBeforeAction, + final ThrowingSupplier action + ) throws Throwable { + arrangeAliasFeatureEnabled(true); + try { + return action.get(); + } finally { + arrangeAliasFeatureEnabled(aliasFeatureEnabledBeforeAction); + } + } + private String getAccessTokenForZone(final String zoneId) throws Exception { final String cacheLookupResult = accessTokenCache.get(zoneId); if (cacheLookupResult != null) { @@ -798,6 +1149,10 @@ private IdentityProvider buildUaaIdpWithAliasProperties( return buildIdpWithAliasProperties(idzId, aliasId, aliasZid, originKey, UAA); } + private void arrangeAliasFeatureEnabled(final boolean enabled) { + ReflectionTestUtils.setField(identityProviderEndpoints, "aliasEntitiesEnabled", enabled); + } + private static IdentityProvider buildIdpWithAliasProperties( final String idzId, final String aliasId, @@ -805,7 +1160,7 @@ private static IdentityProvider buildIdpWithAliasProperties( final String originKey, final String type ) { - final AbstractIdentityProviderDefinition definition = buildIdpDefinition(originKey, type); + final AbstractIdentityProviderDefinition definition = buildIdpDefinition(type); final IdentityProvider provider = new IdentityProvider<>(); provider.setIdentityZoneId(idzId); @@ -819,7 +1174,7 @@ private static IdentityProvider buildIdpWithAliasProperties( return provider; } - private static AbstractIdentityProviderDefinition buildIdpDefinition(final String originKey, final String type) { + private static AbstractIdentityProviderDefinition buildIdpDefinition(final String type) { switch (type) { case OIDC10: final OIDCIdentityProviderDefinition definition = new OIDCIdentityProviderDefinition(); From e21b86278773ac573afd0fe69e1db957539cc759 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 25 Jan 2024 16:47:02 +0100 Subject: [PATCH 81/91] Change log level back to error --- .../identity/uaa/provider/IdentityProviderEndpoints.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3c609e6bf5f..9a8f10fc419 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 @@ -344,7 +344,7 @@ public ResponseEntity testIdentityProvider(@RequestBody IdentityProvider status = BAD_REQUEST; exception = getExceptionString(x); } catch (Exception x) { - logger.warn("Identity provider validation failed.", x); + logger.error("Identity provider validation failed.", x); status = INTERNAL_SERVER_ERROR; exception = "check server logs"; }finally { From 5d48a9d2388852241dea9bb47cee04279c068694 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 25 Jan 2024 17:01:16 +0100 Subject: [PATCH 82/91] Adjust to new UaaTokenUtils.getClaims method --- .../IdentityProviderEndpointsAliasMockMvcTests.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 634456c1097..59623179862 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -25,6 +25,7 @@ import org.cloudfoundry.identity.uaa.DefaultTestContext; import org.cloudfoundry.identity.uaa.mock.util.MockMvcUtils; +import org.cloudfoundry.identity.uaa.oauth.token.Claims; import org.cloudfoundry.identity.uaa.oauth.token.TokenConstants; import org.cloudfoundry.identity.uaa.provider.AbstractExternalOAuthIdentityProviderDefinition; import org.cloudfoundry.identity.uaa.provider.AbstractIdentityProviderDefinition; @@ -679,11 +680,8 @@ private String getAccessTokenForZone(final String zoneId) throws Exception { ); // check if the token contains the expected scopes - final Map claims = UaaTokenUtils.getClaims(accessToken); - assertThat(claims).containsKey("scope"); - assertThat(claims.get("scope")).isInstanceOf(List.class); - final List resultingScopes = (List) claims.get("scope"); - assertThat(resultingScopes).hasSameElementsAs(scopesForZone); + final Claims claims = UaaTokenUtils.getClaimsFromTokenString(accessToken); + assertThat(claims.getScope()).hasSameElementsAs(scopesForZone); // cache the access token accessTokenCache.put(zoneId, accessToken); From af959781369b9ed9de1ba6812ef124b7b6a22030 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 25 Jan 2024 17:53:46 +0100 Subject: [PATCH 83/91] Fix IdentityProviderEndpointsTest --- .../identity/uaa/provider/IdentityProviderEndpoints.java | 7 +++---- .../uaa/provider/IdentityProviderEndpointsTest.java | 3 +++ 2 files changed, 6 insertions(+), 4 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 0ef340617ee..838d7d5e0fe 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 @@ -93,7 +93,8 @@ public class IdentityProviderEndpoints implements ApplicationEventPublisherAware */ private static final Set IDP_TYPES_ALIAS_SUPPORTED = Set.of(SAML, OAUTH20, OIDC10); - private final boolean aliasEntitiesEnabled; + @Value("${uaa.features.aliasEntitiesEnabled:false}") + private boolean aliasEntitiesEnabled; private final IdentityProviderProvisioning identityProviderProvisioning; private final ScimGroupExternalMembershipManager scimGroupExternalMembershipManager; private final ScimGroupProvisioning scimGroupProvisioning; @@ -119,8 +120,7 @@ public IdentityProviderEndpoints( final @Qualifier("identityProviderConfigValidator") IdentityProviderConfigValidator configValidator, final IdentityZoneManager identityZoneManager, final @Qualifier("identityZoneProvisioning") IdentityZoneProvisioning identityZoneProvisioning, - final @Qualifier("transactionManager") PlatformTransactionManager transactionManager, - final @Value("${uaa.features.aliasEntitiesEnabled:false}") boolean aliasEntitiesEnabled + final @Qualifier("transactionManager") PlatformTransactionManager transactionManager ) { this.identityProviderProvisioning = identityProviderProvisioning; this.scimGroupExternalMembershipManager = scimGroupExternalMembershipManager; @@ -130,7 +130,6 @@ public IdentityProviderEndpoints( this.identityZoneManager = identityZoneManager; this.identityZoneProvisioning = identityZoneProvisioning; this.transactionTemplate = new TransactionTemplate(transactionManager); - this.aliasEntitiesEnabled = aliasEntitiesEnabled; } @RequestMapping(method = POST) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index a862dd9ef0d..b7c318690b7 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -35,6 +35,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import org.aspectj.util.Reflection; import org.assertj.core.api.Assertions; import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent; import org.cloudfoundry.identity.uaa.constants.OriginKeys; @@ -56,6 +57,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.PlatformTransactionManager; @ExtendWith(PollutionPreventionExtension.class) @@ -83,6 +85,7 @@ class IdentityProviderEndpointsTest { @BeforeEach void setup() { lenient().when(mockIdentityZoneManager.getCurrentIdentityZoneId()).thenReturn(IdentityZone.getUaaZoneId()); + ReflectionTestUtils.setField(identityProviderEndpoints, "aliasEntitiesEnabled", true); } IdentityProvider getExternalOAuthProvider() { From 1e847bde5f22eb9ca4a3ff6cf9e06d8ff463d4fd Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 29 Jan 2024 08:50:12 +0100 Subject: [PATCH 84/91] Move aliasEntitiesEnabled flag to login --- .../identity/uaa/provider/IdentityProviderEndpoints.java | 2 +- uaa/src/main/resources/uaa.yml | 3 +-- 2 files changed, 2 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 838d7d5e0fe..efdf3e5534f 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 @@ -93,7 +93,7 @@ public class IdentityProviderEndpoints implements ApplicationEventPublisherAware */ private static final Set IDP_TYPES_ALIAS_SUPPORTED = Set.of(SAML, OAUTH20, OIDC10); - @Value("${uaa.features.aliasEntitiesEnabled:false}") + @Value("${login.aliasEntitiesEnabled:false}") private boolean aliasEntitiesEnabled; private final IdentityProviderProvisioning identityProviderProvisioning; private final ScimGroupExternalMembershipManager scimGroupExternalMembershipManager; diff --git a/uaa/src/main/resources/uaa.yml b/uaa/src/main/resources/uaa.yml index 0cdb0f16bab..8e6b54d4898 100755 --- a/uaa/src/main/resources/uaa.yml +++ b/uaa/src/main/resources/uaa.yml @@ -369,6 +369,7 @@ login: # defaultIdentityProvider: uaa # idpDiscoveryEnabled: true # accountChooserEnabled: true +# aliasEntitiesEnabled: true # mfa: # enabled: true @@ -553,8 +554,6 @@ uaa: - GET - HEAD - OPTIONS - features: -# aliasEntitiesEnabled: true #smtp: # host: localhost From f12dc9681915182b769319eb3bd315ad41137fbc Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 29 Jan 2024 09:09:22 +0100 Subject: [PATCH 85/91] Add check for 'active' to IdentityProviderEndpointsAliasMockMvcTests#assertOtherPropertiesAreEqual --- .../uaa/provider/JdbcIdentityProviderProvisioning.java | 4 ++-- .../providers/IdentityProviderEndpointsAliasMockMvcTests.java | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) 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 a8117025f20..89f8eedf1de 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 @@ -94,8 +94,8 @@ public IdentityProvider create(final IdentityProvider identityProvider, String z int pos = 1; ps.setString(pos++, id); ps.setInt(pos++, identityProvider.getVersion()); - ps.setTimestamp(pos++, new Timestamp(System.currentTimeMillis())); - ps.setTimestamp(pos++, new Timestamp(System.currentTimeMillis())); + ps.setTimestamp(pos++, new Timestamp(System.currentTimeMillis())); // created + ps.setTimestamp(pos++, new Timestamp(System.currentTimeMillis())); // lastmodified ps.setString(pos++, identityProvider.getName()); ps.setString(pos++, identityProvider.getOriginKey()); ps.setString(pos++, identityProvider.getType()); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 04138fada11..67599f197ba 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -966,6 +966,9 @@ private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final assertThat(aliasIdp.getOriginKey()).isEqualTo(idp.getOriginKey()); assertThat(aliasIdp.getName()).isEqualTo(idp.getName()); assertThat(aliasIdp.getType()).isEqualTo(idp.getType()); + assertThat(aliasIdp.isActive()).isEqualTo(idp.isActive()); + + // it is expected that the two entities have differing values for 'lastmodified', 'created' and 'version' } private IdentityProvider createIdpWithAlias(final IdentityZone zone1, final IdentityZone zone2) throws Exception { From a6ac355931811a573a8889c25e0ac74d909b8b9b Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 29 Jan 2024 11:11:27 +0100 Subject: [PATCH 86/91] Add unit tests for handling disabled alias entities feature --- .../IdentityProviderEndpointsTest.java | 181 +++++++++++++++++- 1 file changed, 178 insertions(+), 3 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index b7c318690b7..a6a2e7026bf 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -35,7 +35,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; -import org.aspectj.util.Reflection; +import org.apache.commons.lang3.tuple.Pair; import org.assertj.core.api.Assertions; import org.cloudfoundry.identity.uaa.audit.event.EntityDeletedEvent; import org.cloudfoundry.identity.uaa.constants.OriginKeys; @@ -85,7 +85,7 @@ class IdentityProviderEndpointsTest { @BeforeEach void setup() { lenient().when(mockIdentityZoneManager.getCurrentIdentityZoneId()).thenReturn(IdentityZone.getUaaZoneId()); - ReflectionTestUtils.setField(identityProviderEndpoints, "aliasEntitiesEnabled", true); + arrangeAliasEntitiesEnabled(true); } IdentityProvider getExternalOAuthProvider() { @@ -394,7 +394,7 @@ void testUpdateIdentityProvider_ShouldRejectInvalidReferenceToAliasInExistingIdp // arrange existing IdP with invalid reference to alias IdP: alias ZID, but alias ID not final String existingIdpId = UUID.randomUUID().toString(); - final IdentityProvider existingIdp = getLdapDefinition(); + final IdentityProvider existingIdp = getExternalOAuthProvider(); existingIdp.setId(existingIdpId); existingIdp.setAliasZid(customZoneId); when(mockIdentityProviderProvisioning.retrieve(existingIdpId, IdentityZone.getUaaZoneId())) @@ -410,6 +410,75 @@ void testUpdateIdentityProvider_ShouldRejectInvalidReferenceToAliasInExistingIdp ); } + @Test + void testUpdateIdpWithExistingAlias_ShouldBreakReferenceIfAliasFeatureDisabled() throws MetadataProviderException { + arrangeAliasEntitiesEnabled(false); + + final String zone1Id = UAA; + final String zone2Id = UUID.randomUUID().toString(); + + final Pair, IdentityProvider> idpAndAlias = arrangeOidcIdpWithAliasExists(zone1Id, zone2Id); + final IdentityProvider idp = idpAndAlias.getLeft(); + final IdentityProvider aliasIdp = idpAndAlias.getRight(); + + when(mockIdentityProviderProvisioning.update(any(), anyString())).thenAnswer(invocationOnMock -> + invocationOnMock.getArgument(0) + ); + + // update name; both alias properties must be set to null since the feature was disabled in the meantime + final IdentityProvider requestBody = shallowCloneIdp(idp); + requestBody.setName("some-new-name"); + requestBody.setAliasId(null); + requestBody.setAliasZid(null); + identityProviderEndpoints.updateIdentityProvider(requestBody.getId(), requestBody, true); + + final ArgumentCaptor updateIdpParamCaptor = ArgumentCaptor.forClass(IdentityProvider.class); + final ArgumentCaptor updateZidParamCaptor = ArgumentCaptor.forClass(String.class); + verify(mockIdentityProviderProvisioning, times(2)).update(updateIdpParamCaptor.capture(), updateZidParamCaptor.capture()); + + // first call: should update original IdP regularly + final IdentityProvider idpUpdateCall1 = updateIdpParamCaptor.getAllValues().get(0); + final String zidUpdateCall1 = updateZidParamCaptor.getAllValues().get(0); + Assertions.assertThat(idpUpdateCall1).isEqualTo(requestBody); + Assertions.assertThat(zidUpdateCall1).isEqualTo(zone1Id); + + // second call: should remove alias properties in alias IdP (and leave other properties unchanged) + final IdentityProvider idpUpdateCall2 = updateIdpParamCaptor.getAllValues().get(1); + final String zidUpdateCall2 = updateZidParamCaptor.getAllValues().get(1); + Assertions.assertThat(zidUpdateCall2).isEqualTo(zone2Id); + Assertions.assertThat(idpUpdateCall2).isNotNull(); + Assertions.assertThat(idpUpdateCall2.getAliasId()).isBlank(); + Assertions.assertThat(idpUpdateCall2.getAliasZid()).isBlank(); + // apart from the alias properties, the alias IdP should be left unchanged + aliasIdp.setAliasId(null); + idpUpdateCall2.setAliasId(null); + aliasIdp.setAliasZid(null); + idpUpdateCall2.setAliasZid(null); + Assertions.assertThat(idpUpdateCall2).isEqualTo(aliasIdp); + } + + @Test + void testUpdateIdpWithExistingAlias_ShouldRejectIfAliasFeatureDisabledAndAliasPropsNonNull() { + final String customZoneId = UUID.randomUUID().toString(); + + // arrange existing IdP with alias + final String existingIdpId = UUID.randomUUID().toString(); + final IdentityProvider existingIdp = getExternalOAuthProvider(); + existingIdp.setId(existingIdpId); + existingIdp.setAliasZid(customZoneId); + when(mockIdentityProviderProvisioning.retrieve(existingIdpId, IdentityZone.getUaaZoneId())) + .thenReturn(existingIdp); + + final IdentityProvider requestBody = getExternalOAuthProvider(); + requestBody.setId(existingIdpId); + requestBody.setAliasZid(customZoneId); + requestBody.setName("new-name"); + + Assertions.assertThatIllegalStateException().isThrownBy(() -> + identityProviderEndpoints.updateIdentityProvider(existingIdpId, requestBody, true) + ); + } + @Test void testUpdateIdpWithExistingAlias_ValidChange() throws MetadataProviderException { final String existingIdpId = UUID.randomUUID().toString(); @@ -526,6 +595,18 @@ void testCreateIdentityProvider_AliasNotSupportedForType() throws MetadataProvid Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); } + @Test + void testCreateIdentityProvider_ShouldRejectNonNullAliasZidIfAliasFeatureDisabled() throws MetadataProviderException { + arrangeAliasEntitiesEnabled(false); + + // create valid IdP with alias zid set + final IdentityProvider idp = getExternalOAuthProvider(); + idp.setAliasZid(UUID.randomUUID().toString()); + + final ResponseEntity response = identityProviderEndpoints.createIdentityProvider(idp, true); + Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + } + @Test void testCreateIdentityProvider_ValidAliasProperties() throws MetadataProviderException { // arrange custom zone exists @@ -774,6 +855,76 @@ void testDeleteIdpWithAlias_DanglingReference() { Assertions.assertThat(((IdentityProvider) firstEvent.getSource()).getId()).isEqualTo(idpId); } + @Test + void testDeleteIdpWithAlias_AliasFeatureDisabled() { + arrangeAliasEntitiesEnabled(false); + + // arrange IdP with alias exists + final String customZoneId = UUID.randomUUID().toString(); + final Pair, IdentityProvider> idpAndAlias = arrangeOidcIdpWithAliasExists(UAA, customZoneId); + final IdentityProvider idp = idpAndAlias.getLeft(); + final IdentityProvider aliasIdp = idpAndAlias.getRight(); + + final ApplicationEventPublisher mockEventPublisher = mock(ApplicationEventPublisher.class); + identityProviderEndpoints.setApplicationEventPublisher(mockEventPublisher); + doNothing().when(mockEventPublisher).publishEvent(any()); + + identityProviderEndpoints.deleteIdentityProvider(idp.getId(), true); + + // the original IdP should be deleted + final ArgumentCaptor> entityDeletedEventCaptor = ArgumentCaptor.forClass(EntityDeletedEvent.class); + verify(mockEventPublisher, times(1)).publishEvent(entityDeletedEventCaptor.capture()); + final EntityDeletedEvent event = entityDeletedEventCaptor.getValue(); + Assertions.assertThat(event).isNotNull(); + Assertions.assertThat(event.getIdentityZoneId()).isEqualTo(UAA); + Assertions.assertThat(((IdentityProvider) event.getSource()).getId()).isEqualTo(idp.getId()); + + // instead of being deleted, the alias IdP should just have its reference to the original IdP removed + final ArgumentCaptor updateIdpParamCaptor = ArgumentCaptor.forClass(IdentityProvider.class); + verify(mockIdentityProviderProvisioning).update(updateIdpParamCaptor.capture(), eq(customZoneId)); + final IdentityProvider updateIdpParam = updateIdpParamCaptor.getValue(); + Assertions.assertThat(updateIdpParam).isNotNull(); + Assertions.assertThat(updateIdpParam.getAliasId()).isBlank(); + Assertions.assertThat(updateIdpParam.getAliasZid()).isBlank(); + // apart from aliasId and aliasZid, the alias IdP should be left unchanged + aliasIdp.setAliasZid(null); + updateIdpParam.setAliasZid(null); + aliasIdp.setAliasId(null); + updateIdpParam.setAliasId(null); + Assertions.assertThat(updateIdpParam).isEqualTo(aliasIdp); + } + + private Pair, IdentityProvider> arrangeOidcIdpWithAliasExists( + final String zone1Id, + final String zone2Id + ) { + Assertions.assertThat(zone1Id).isNotBlank(); + Assertions.assertThat(zone2Id).isNotBlank().isNotEqualTo(zone1Id); + + final String idpId = UUID.randomUUID().toString(); + final String aliasIdpId = UUID.randomUUID().toString(); + + // arrange original IdP exists in zone 1 + final IdentityProvider idp = new IdentityProvider<>(); + idp.setType(OIDC10); + idp.setId(idpId); + idp.setIdentityZoneId(zone1Id); + idp.setAliasId(aliasIdpId); + idp.setAliasZid(zone2Id); + when(mockIdentityProviderProvisioning.retrieve(idpId, zone1Id)).thenReturn(idp); + + // arrange alias IdP exists in zone 2 + final IdentityProvider aliasIdp = new IdentityProvider<>(); + aliasIdp.setType(OIDC10); + aliasIdp.setId(aliasIdpId); + aliasIdp.setIdentityZoneId(zone2Id); + aliasIdp.setAliasId(idpId); + aliasIdp.setAliasZid(zone1Id); + when(mockIdentityProviderProvisioning.retrieve(aliasIdpId, zone2Id)).thenReturn(aliasIdp); + + return Pair.of(idp, aliasIdp); + } + @Test void testDeleteIdentityProviderNotExisting() { String zoneId = IdentityZone.getUaaZoneId(); @@ -829,4 +980,28 @@ void testDeleteIdentityProviderResponseNotContainingBindPassword() { assertNull(((LdapIdentityProviderDefinition)deleteResponse .getBody().getConfig()).getBindPassword()); } + + private void arrangeAliasEntitiesEnabled(final boolean enabled) { + ReflectionTestUtils.setField(identityProviderEndpoints, "aliasEntitiesEnabled", enabled); + } + + private static IdentityProvider shallowCloneIdp( + final IdentityProvider idp + ) { + final IdentityProvider cloneIdp = new IdentityProvider<>(); + cloneIdp.setId(idp.getId()); + cloneIdp.setName(idp.getName()); + cloneIdp.setConfig(idp.getConfig()); + cloneIdp.setType(idp.getType()); + cloneIdp.setCreated(idp.getCreated()); + cloneIdp.setLastModified(idp.getLastModified()); + cloneIdp.setIdentityZoneId(idp.getIdentityZoneId()); + cloneIdp.setAliasId(idp.getAliasId()); + cloneIdp.setAliasZid(idp.getAliasZid()); + cloneIdp.setActive(idp.isActive()); + + Assertions.assertThat(cloneIdp).isEqualTo(idp); + + return cloneIdp; + } } From 9ea5973ae7dfbb504b09e7a6ee6bef2bcedc7b92 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 29 Jan 2024 11:16:37 +0100 Subject: [PATCH 87/91] Refactor --- .../IdentityProviderEndpointsTest.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index a6a2e7026bf..46c25c854b7 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -449,12 +449,7 @@ void testUpdateIdpWithExistingAlias_ShouldBreakReferenceIfAliasFeatureDisabled() Assertions.assertThat(idpUpdateCall2).isNotNull(); Assertions.assertThat(idpUpdateCall2.getAliasId()).isBlank(); Assertions.assertThat(idpUpdateCall2.getAliasZid()).isBlank(); - // apart from the alias properties, the alias IdP should be left unchanged - aliasIdp.setAliasId(null); - idpUpdateCall2.setAliasId(null); - aliasIdp.setAliasZid(null); - idpUpdateCall2.setAliasZid(null); - Assertions.assertThat(idpUpdateCall2).isEqualTo(aliasIdp); + assertIdpsAreEqualApartFromAliasProperties(idpUpdateCall2, aliasIdp); } @Test @@ -886,12 +881,7 @@ void testDeleteIdpWithAlias_AliasFeatureDisabled() { Assertions.assertThat(updateIdpParam).isNotNull(); Assertions.assertThat(updateIdpParam.getAliasId()).isBlank(); Assertions.assertThat(updateIdpParam.getAliasZid()).isBlank(); - // apart from aliasId and aliasZid, the alias IdP should be left unchanged - aliasIdp.setAliasZid(null); - updateIdpParam.setAliasZid(null); - aliasIdp.setAliasId(null); - updateIdpParam.setAliasId(null); - Assertions.assertThat(updateIdpParam).isEqualTo(aliasIdp); + assertIdpsAreEqualApartFromAliasProperties(updateIdpParam, aliasIdp); } private Pair, IdentityProvider> arrangeOidcIdpWithAliasExists( @@ -1004,4 +994,15 @@ private static IdentityProvider idp1, + final IdentityProvider idp2 + ) { + idp2.setAliasId(null); + idp1.setAliasId(null); + idp2.setAliasZid(null); + idp1.setAliasZid(null); + Assertions.assertThat(idp1).isEqualTo(idp2); + } } From 291144cce7ae7cbbd976300f9adebc6a6159b8cf Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Mon, 29 Jan 2024 11:21:57 +0100 Subject: [PATCH 88/91] Refactor --- .../IdentityProviderEndpointsAliasMockMvcTests.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java index 67599f197ba..76ab29006a7 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/mock/providers/IdentityProviderEndpointsAliasMockMvcTests.java @@ -949,14 +949,14 @@ private void assertReferenceWasRemovedFromAlias(final String aliasId, final Stri assertThat(aliasIdpAfterDeletion.get().getAliasZid()).isBlank(); } - private void assertIdpReferencesOtherIdp(final IdentityProvider idp, final IdentityProvider referencedIdp) { + private static void assertIdpReferencesOtherIdp(final IdentityProvider idp, final IdentityProvider referencedIdp) { assertThat(idp).isNotNull(); assertThat(referencedIdp).isNotNull(); assertThat(referencedIdp.getId()).isNotBlank().isEqualTo(idp.getAliasId()); assertThat(referencedIdp.getIdentityZoneId()).isNotBlank().isEqualTo(idp.getAliasZid()); } - private void assertOtherPropertiesAreEqual(final IdentityProvider idp, final IdentityProvider aliasIdp) { + private static void assertOtherPropertiesAreEqual(final IdentityProvider idp, final IdentityProvider aliasIdp) { // the configs should be identical final OIDCIdentityProviderDefinition originalIdpConfig = (OIDCIdentityProviderDefinition) idp.getConfig(); final OIDCIdentityProviderDefinition aliasIdpConfig = (OIDCIdentityProviderDefinition) aliasIdp.getConfig(); @@ -1120,7 +1120,7 @@ private Optional> readIdpViaDb(final String id, final String return Optional.of(idp); } - private void assertRelyingPartySecretIsRedacted(final IdentityProvider identityProvider) { + private static void assertRelyingPartySecretIsRedacted(final IdentityProvider identityProvider) { assertThat(identityProvider.getType()).isEqualTo(OIDC10); final Optional> config = Optional.ofNullable(identityProvider.getConfig()) .map(it -> (AbstractExternalOAuthIdentityProviderDefinition) it); @@ -1141,7 +1141,7 @@ private static IdentityProvider buildOidcIdpWithAliasProperties( return buildIdpWithAliasProperties(idzId, aliasId, aliasZid, originKey, OIDC10); } - private IdentityProvider buildUaaIdpWithAliasProperties( + private static IdentityProvider buildUaaIdpWithAliasProperties( final String idzId, final String aliasId, final String aliasZid From e5f579179e6d1c0486cd814ac6b817cbae848b21 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 8 Feb 2024 10:07:25 +0100 Subject: [PATCH 89/91] Fix Sonar finding --- .../provider/IdentityProviderEndpoints.java | 30 ++++++++++--- .../uaa/provider/IdpAliasFailedException.java | 43 +++++++++++++++---- 2 files changed, 58 insertions(+), 15 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 efdf3e5534f..fe3a6c50ac6 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 @@ -541,11 +541,29 @@ private IdentityProvider ensur return originalIdp; } + final IdentityProvider persistedAliasIdp = createNewAliasIdp(aliasIdp, originalIdp.getAliasZid()); + + // update alias ID in original IdP + originalIdp.setAliasId(persistedAliasIdp.getId()); + return identityProviderProvisioning.update(originalIdp, originalIdp.getIdentityZoneId()); + } + + /** + * Persist the given alias IdP in the given zone. + * + * @param aliasIdp the alias IdP to persist + * @param aliasZid the ID of the identity zone in which the alias should be persisted + * @return the persisted alias IdP + */ + private IdentityProvider createNewAliasIdp( + final IdentityProvider aliasIdp, + final String aliasZid + ) throws IdpAliasFailedException { // check if IdZ referenced in 'aliasZid' exists try { - identityZoneProvisioning.retrieve(originalIdp.getAliasZid()); + identityZoneProvisioning.retrieve(aliasZid); } catch (final ZoneDoesNotExistsException e) { - throw new IdpAliasFailedException(originalIdp, ALIAS_ZONE_DOES_NOT_EXIST, e); + throw new IdpAliasFailedException(aliasIdp.getAliasId(), aliasIdp.getAliasZid(), null, aliasZid, ALIAS_ZONE_DOES_NOT_EXIST, e); } // create new alias IdP in alias zid @@ -553,15 +571,13 @@ private IdentityProvider ensur try { persistedAliasIdp = identityProviderProvisioning.create( aliasIdp, - originalIdp.getAliasZid() + aliasZid ); } catch (final IdpAlreadyExistsException e) { - throw new IdpAliasFailedException(originalIdp, ORIGIN_KEY_ALREADY_USED_IN_ALIAS_ZONE, e); + throw new IdpAliasFailedException(aliasIdp.getAliasId(), aliasIdp.getAliasZid(), null, aliasZid, ORIGIN_KEY_ALREADY_USED_IN_ALIAS_ZONE, e); } - // update alias ID in original IdP - originalIdp.setAliasId(persistedAliasIdp.getId()); - return identityProviderProvisioning.update(originalIdp, originalIdp.getIdentityZoneId()); + return persistedAliasIdp; } @Nullable diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java index d545be9689d..1793c2a80ce 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/provider/IdpAliasFailedException.java @@ -15,24 +15,51 @@ public IdpAliasFailedException( @NonNull final Reason reason, @Nullable final Throwable cause ) { - super(cause, ERROR, buildMessage(identityProvider, reason), reason.responseCode.value()); + this( + identityProvider.getId(), + identityProvider.getIdentityZoneId(), + identityProvider.getAliasId(), + identityProvider.getAliasZid(), + reason, + cause + ); + } + + public IdpAliasFailedException( + @Nullable final String idpId, + @Nullable final String idzId, + @Nullable final String aliasId, + @Nullable final String aliasZid, + @NonNull final Reason reason, + @Nullable final Throwable cause + ) { + super(cause, ERROR, buildMessage(idpId, idzId, aliasId, aliasZid, reason), reason.responseCode.value()); } - private static String buildMessagePrefix(@NonNull final IdentityProvider idp) { + private static String buildMessagePrefix( + @Nullable final String idpId, + @Nullable final String idzId, + @Nullable final String aliasId, + @Nullable final String aliasZid + ) { return String.format( "IdentityProvider[id=%s,zid=%s,aliasId=%s,aliasZid=%s]", - surroundWithSingleQuotesIfPresent(idp.getId()), - surroundWithSingleQuotesIfPresent(idp.getIdentityZoneId()), - surroundWithSingleQuotesIfPresent(idp.getAliasId()), - surroundWithSingleQuotesIfPresent(idp.getAliasZid()) + surroundWithSingleQuotesIfPresent(idpId), + surroundWithSingleQuotesIfPresent(idzId), + surroundWithSingleQuotesIfPresent(aliasId), + surroundWithSingleQuotesIfPresent(aliasZid) ); } private static String buildMessage( - @NonNull final IdentityProvider idp, + @Nullable final String idpId, + @Nullable final String idzId, + @Nullable final String aliasId, + @Nullable final String aliasZid, @NonNull final Reason reason ) { - return String.format("%s - %s", buildMessagePrefix(idp), reason.message); + final String messagePrefix = buildMessagePrefix(idpId, idzId, aliasId, aliasZid); + return String.format("%s - %s", messagePrefix, reason.message); } private static String surroundWithSingleQuotesIfPresent(@Nullable final String input) { From 9515079d4908c4b27ff89b6c6c2942da354e6586 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 8 Feb 2024 10:13:12 +0100 Subject: [PATCH 90/91] Fix Flyway migration --- ...ider_Add_Alias.sql => V4_106__Identity_Provider_Add_Alias.sql} | 0 ...ider_Add_Alias.sql => V4_106__Identity_Provider_Add_Alias.sql} | 0 ...ider_Add_Alias.sql => V4_106__Identity_Provider_Add_Alias.sql} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/{V4_105__Identity_Provider_Add_Alias.sql => V4_106__Identity_Provider_Add_Alias.sql} (100%) rename server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/{V4_105__Identity_Provider_Add_Alias.sql => V4_106__Identity_Provider_Add_Alias.sql} (100%) rename server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/{V4_105__Identity_Provider_Add_Alias.sql => V4_106__Identity_Provider_Add_Alias.sql} (100%) diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V4_105__Identity_Provider_Add_Alias.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V4_106__Identity_Provider_Add_Alias.sql similarity index 100% rename from server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V4_105__Identity_Provider_Add_Alias.sql rename to server/src/main/resources/org/cloudfoundry/identity/uaa/db/hsqldb/V4_106__Identity_Provider_Add_Alias.sql diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_105__Identity_Provider_Add_Alias.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_106__Identity_Provider_Add_Alias.sql similarity index 100% rename from server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_105__Identity_Provider_Add_Alias.sql rename to server/src/main/resources/org/cloudfoundry/identity/uaa/db/mysql/V4_106__Identity_Provider_Add_Alias.sql diff --git a/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V4_105__Identity_Provider_Add_Alias.sql b/server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V4_106__Identity_Provider_Add_Alias.sql similarity index 100% rename from server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V4_105__Identity_Provider_Add_Alias.sql rename to server/src/main/resources/org/cloudfoundry/identity/uaa/db/postgresql/V4_106__Identity_Provider_Add_Alias.sql From fafe033a4a90f91c8991544ba83f24fbae484157 Mon Sep 17 00:00:00 2001 From: Adrian Hoelzl Date: Thu, 8 Feb 2024 15:01:34 +0100 Subject: [PATCH 91/91] Add unit tests about missing aliasId or missing alias IdP during breaking the alias reference --- .../IdentityProviderEndpointsTest.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java index 46c25c854b7..e74ed61a4eb 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/provider/IdentityProviderEndpointsTest.java @@ -55,6 +55,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.opensaml.saml2.metadata.provider.MetadataProviderException; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.util.ReflectionTestUtils; @@ -452,6 +453,85 @@ void testUpdateIdpWithExistingAlias_ShouldBreakReferenceIfAliasFeatureDisabled() assertIdpsAreEqualApartFromAliasProperties(idpUpdateCall2, aliasIdp); } + @Test + void testUpdateIdpWithExistingAlias_AliasFeatureDisabled_ShouldIgnoreMissingAliasId() throws MetadataProviderException { + final String zone1Id = UAA; + final String zone2Id = UUID.randomUUID().toString(); + + final String idpId = UUID.randomUUID().toString(); + final String aliasIdpId = UUID.randomUUID().toString(); + + // arrange original IdP exists in zone 1 + final IdentityProvider idp = new IdentityProvider<>(); + idp.setType(OIDC10); + idp.setId(idpId); + idp.setIdentityZoneId(zone1Id); + idp.setAliasId(aliasIdpId); + idp.setAliasZid(zone2Id); + + // arrange existing IdP has no alias ID + final IdentityProvider existingIdp = shallowCloneIdp(idp); + existingIdp.setAliasId(null); + when(mockIdentityProviderProvisioning.retrieve(idpId, zone1Id)).thenReturn(existingIdp); + + // mock update of original IdP + when(mockIdentityProviderProvisioning.update(idp, zone1Id)) + .then(invocationOnMock -> invocationOnMock.getArgument(0)); + + // alias feature is now disabled + arrangeAliasEntitiesEnabled(false); + + // update the original IdP -> reference to alias will be broken, alias will be ignored + idp.setName("some-new-name"); + idp.setAliasId(null); + idp.setAliasZid(null); + final ResponseEntity response = identityProviderEndpoints.updateIdentityProvider(idpId, idp, true); + Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + final IdentityProvider responseBody = response.getBody(); + Assertions.assertThat(responseBody).isEqualTo(idp); + } + + @Test + void testUpdateIdpWithExistingAlias_AliasFeatureDisabled_ShouldIgnoreMissingAlias() throws MetadataProviderException { + final String zone1Id = UAA; + final String zone2Id = UUID.randomUUID().toString(); + + final String idpId = UUID.randomUUID().toString(); + final String aliasIdpId = UUID.randomUUID().toString(); + + // arrange original IdP exists in zone 1 + final IdentityProvider idp = new IdentityProvider<>(); + idp.setType(OIDC10); + idp.setId(idpId); + idp.setIdentityZoneId(zone1Id); + idp.setAliasId(aliasIdpId); + idp.setAliasZid(zone2Id); + + // arrange existing IdP has no alias ID + final IdentityProvider existingIdp = shallowCloneIdp(idp); + when(mockIdentityProviderProvisioning.retrieve(idpId, zone1Id)).thenReturn(existingIdp); + + // arrange alias IdP does not exist + when(mockIdentityProviderProvisioning.retrieve(aliasIdpId, zone2Id)) + .thenThrow(new EmptyResultDataAccessException(1)); + + // mock update of original IdP + when(mockIdentityProviderProvisioning.update(idp, zone1Id)) + .then(invocationOnMock -> invocationOnMock.getArgument(0)); + + // alias feature is now disabled + arrangeAliasEntitiesEnabled(false); + + // update the original IdP -> reference to alias will be broken, alias will be ignored + idp.setName("some-new-name"); + idp.setAliasId(null); + idp.setAliasZid(null); + final ResponseEntity response = identityProviderEndpoints.updateIdentityProvider(idpId, idp, true); + Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + final IdentityProvider responseBody = response.getBody(); + Assertions.assertThat(responseBody).isEqualTo(idp); + } + @Test void testUpdateIdpWithExistingAlias_ShouldRejectIfAliasFeatureDisabledAndAliasPropsNonNull() { final String customZoneId = UUID.randomUUID().toString();