diff --git a/.rhcicd/clowdapp-connector-email.yaml b/.rhcicd/clowdapp-connector-email.yaml index 4f6aadbbd9..528ca8b0a1 100644 --- a/.rhcicd/clowdapp-connector-email.yaml +++ b/.rhcicd/clowdapp-connector-email.yaml @@ -70,6 +70,8 @@ objects: env: - name: ENV_NAME value: ${ENV_NAME} + - name: NOTIFICATIONS_CONNECTOR_BOP_SKIP_USERS_RESOLUTION + value: ${NOTIFICATIONS_CONNECTOR_BOP_SKIP_USERS_RESOLUTION} - name: NOTIFICATIONS_CONNECTOR_ENDPOINT_CACHE_MAX_SIZE value: ${NOTIFICATIONS_CONNECTOR_ENDPOINT_CACHE_MAX_SIZE} - name: NOTIFICATIONS_CONNECTOR_FETCH_USERS_RBAC_ENABLED @@ -205,6 +207,9 @@ parameters: - name: MIN_REPLICAS value: "3" +- name: NOTIFICATIONS_CONNECTOR_BOP_SKIP_USERS_RESOLUTION + description: Should BOP skip transforming usernames from our payload into email addresses using the IT Users Service? + value: "false" - name: NOTIFICATIONS_CONNECTOR_ENDPOINT_CACHE_MAX_SIZE description: Maximum size of the Camel endpoints cache value: "100" diff --git a/.rhcicd/clowdapp-engine.yaml b/.rhcicd/clowdapp-engine.yaml index cc6213e45d..3f50884914 100644 --- a/.rhcicd/clowdapp-engine.yaml +++ b/.rhcicd/clowdapp-engine.yaml @@ -150,6 +150,8 @@ objects: key: client-id - name: PROCESSOR_EMAIL_BOP_ENV value: ${BACKOFFICE_CLIENT_ENV} + - name: PROCESSOR_EMAIL_BOP_SKIP_USERS_RESOLUTION + value: ${PROCESSOR_EMAIL_BOP_SKIP_USERS_RESOLUTION} - name: PROCESSOR_EMAIL_BOP_URL value: ${BACKOFFICE_SCHEME}://${BACKOFFICE_HOST}:${BACKOFFICE_PORT}/v1/sendEmails - name: PROCESSOR_EMAIL_NO_REPLY @@ -330,6 +332,9 @@ parameters: - name: NOTIFICATIONS_LOG_LEVEL description: Log level for com.redhat.cloud.notifications value: INFO +- name: PROCESSOR_EMAIL_BOP_SKIP_USERS_RESOLUTION + description: Should BOP skip transforming usernames from our payload into email addresses using the IT Users Service? + value: "false" - name: QUARKUS_HIBERNATE_ORM_LOG_SQL value: "false" - name: QUARKUS_LOG_CLOUDWATCH_API_CALL_TIMEOUT diff --git a/common/src/main/java/com/redhat/cloud/notifications/config/FeatureFlipper.java b/common/src/main/java/com/redhat/cloud/notifications/config/FeatureFlipper.java index dcde47bdfa..c19df72d45 100644 --- a/common/src/main/java/com/redhat/cloud/notifications/config/FeatureFlipper.java +++ b/common/src/main/java/com/redhat/cloud/notifications/config/FeatureFlipper.java @@ -98,6 +98,9 @@ public class FeatureFlipper { @ConfigProperty(name = "notifications.async-aggregation.enabled", defaultValue = "true") boolean asyncAggregation; + @ConfigProperty(name = "processor.email.bop.skip-users-resolution", defaultValue = "false") + boolean skipBopUsersResolution; + void logFeaturesStatusAtStartup(@Observes StartupEvent event) { Log.infof("=== %s startup status ===", FeatureFlipper.class.getSimpleName()); Log.infof("The behavior groups unique name constraint is %s", enforceBehaviorGroupNameUnicity ? "enabled" : "disabled"); @@ -120,6 +123,7 @@ void logFeaturesStatusAtStartup(@Observes StartupEvent event) { Log.infof("The email connector is %s", emailConnectorEnabled ? "enabled" : "disabled"); Log.infof("The drawer connector is %s", drawerConnectorEnabled ? "enabled" : "disabled"); Log.infof("The async aggregation is %s", asyncAggregation ? "enabled" : "disabled"); + Log.infof("The BOP users resolution is %s", !skipBopUsersResolution ? "enabled" : "disabled"); } public boolean isEnforceBehaviorGroupNameUnicity() { @@ -298,6 +302,15 @@ public void setAsyncAggregation(boolean asyncAggregation) { this.asyncAggregation = asyncAggregation; } + public boolean isSkipBopUsersResolution() { + return skipBopUsersResolution; + } + + public void setSkipBopUsersResolution(boolean skipBopUsersResolution) { + checkTestLaunchMode(); + this.skipBopUsersResolution = skipBopUsersResolution; + } + /** * This method throws an {@link IllegalStateException} if it is invoked with a launch mode different from * {@link io.quarkus.runtime.LaunchMode#TEST TEST}. It should be added to methods that allow overriding a diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/EmailRouteBuilder.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/EmailRouteBuilder.java index de9fd60282..2a182d0a32 100644 --- a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/EmailRouteBuilder.java +++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/EmailRouteBuilder.java @@ -7,6 +7,7 @@ import com.redhat.cloud.notifications.connector.email.config.Environment; import com.redhat.cloud.notifications.connector.email.constants.ExchangeProperty; import com.redhat.cloud.notifications.connector.email.constants.Routes; +import com.redhat.cloud.notifications.connector.email.model.settings.User; import com.redhat.cloud.notifications.connector.email.predicates.NotFinishedFetchingAllPages; import com.redhat.cloud.notifications.connector.email.predicates.rbac.StatusCodeNotFound; import com.redhat.cloud.notifications.connector.email.processors.bop.BOPRequestPreparer; @@ -154,10 +155,10 @@ public void configureRoute() throws Exception { from(seda(ENGINE_TO_CONNECTOR)) .routeId(this.connectorConfig.getConnectorName()) - // Initialize the usernames hash set, where we will gather the + // Initialize the users hash set, where we will gather the // fetched users from the user providers. - .process(exchange -> exchange.setProperty(ExchangeProperty.USERNAMES, new HashSet())) - // Split each recipient setting and aggregate the usernames to end + .process(exchange -> exchange.setProperty(ExchangeProperty.USERS, new HashSet())) + // Split each recipient setting and aggregate the users to end // up with a single exchange. .split(simpleF("${exchangeProperty.%s}", ExchangeProperty.RECIPIENT_SETTINGS), this.userAggregationStrategy).stopOnException() // As the body of the exchange might change throughout the @@ -195,13 +196,13 @@ public void configureRoute() throws Exception { .setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_GET)) .setHeader(CaffeineConstants.KEY, this.computeCacheKey()) .to(caffeineCache(Routes.FETCH_USERS_RBAC)) - // Avoid calling RBAC if we do have the usernames cached. + // Avoid calling RBAC if we do have the users cached. .choice() .when(header(CaffeineConstants.ACTION_HAS_RESULT).isEqualTo(Boolean.TRUE)) - // The cache engine leaves the usernames in the body of the + // The cache engine leaves the users in the body of the // exchange, that is why we need to set them back in the // property that the subsequent processors expect to find them. - .setProperty(ExchangeProperty.USERNAMES, body()) + .setProperty(ExchangeProperty.USERS, body()) .otherwise() // Clear all the headers that may come from the previous route. .removeHeaders("*") @@ -220,7 +221,7 @@ public void configureRoute() throws Exception { // Store all the received recipients in the cache. .setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_PUT)) .setHeader(CaffeineConstants.KEY, this.computeCacheKey()) - .setHeader(CaffeineConstants.VALUE, exchangeProperty(ExchangeProperty.USERNAMES)) + .setHeader(CaffeineConstants.VALUE, exchangeProperty(ExchangeProperty.USERS)) .to(caffeineCache(Routes.FETCH_USERS_RBAC)) .endChoice() .end() @@ -243,13 +244,13 @@ public void configureRoute() throws Exception { .setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_GET)) .setHeader(CaffeineConstants.KEY, this.computeCacheKey()) .to(caffeineCache(Routes.FETCH_USERS_IT)) - // Avoid calling IT if we do have the usernames cached. + // Avoid calling IT if we do have the users cached. .choice() .when(header(CaffeineConstants.ACTION_HAS_RESULT).isEqualTo(Boolean.TRUE)) - // The cache engine leaves the usernames in the body of the + // The cache engine leaves the users in the body of the // exchange, that is why we need to set them back in the // property that the subsequent processors expect to find them. - .setProperty(ExchangeProperty.USERNAMES, body()) + .setProperty(ExchangeProperty.USERS, body()) .otherwise() // Clear all the headers that may come from the previous route. .removeHeaders("*") @@ -266,7 +267,7 @@ public void configureRoute() throws Exception { // Store all the received recipients in the cache. .setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_PUT)) .setHeader(CaffeineConstants.KEY, this.computeCacheKey()) - .setHeader(CaffeineConstants.VALUE, exchangeProperty(ExchangeProperty.USERNAMES)) + .setHeader(CaffeineConstants.VALUE, exchangeProperty(ExchangeProperty.USERS)) .to(caffeineCache(Routes.FETCH_USERS_IT)) .endChoice() .end() @@ -306,13 +307,13 @@ public void configureRoute() throws Exception { .setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_GET)) .setHeader(CaffeineConstants.KEY, this.computeGroupPrincipalsCacheKey()) .to(caffeineCache(Routes.FETCH_GROUP_USERS)) - // Avoid calling RBAC if we do have the usernames cached. + // Avoid calling RBAC if we do have the users cached. .choice() .when(header(CaffeineConstants.ACTION_HAS_RESULT).isEqualTo(Boolean.TRUE)) - // The cache engine leaves the usernames in the body of the + // The cache engine leaves the users in the body of the // exchange, that is why we need to set them back in the // property that the subsequent processors expect to find them. - .setProperty(ExchangeProperty.USERNAMES, body()) + .setProperty(ExchangeProperty.USERS, body()) .otherwise() // Clear all the headers that may come from the previous route. .removeHeaders("*") @@ -328,7 +329,7 @@ public void configureRoute() throws Exception { // Store all the received recipients in the cache. .setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_PUT)) .setHeader(CaffeineConstants.KEY, this.computeGroupPrincipalsCacheKey()) - .setHeader(CaffeineConstants.VALUE, exchangeProperty(ExchangeProperty.USERNAMES)) + .setHeader(CaffeineConstants.VALUE, exchangeProperty(ExchangeProperty.USERS)) .to(caffeineCache(Routes.FETCH_GROUP_USERS)) .endChoice() .end() @@ -366,7 +367,7 @@ public void configureRoute() throws Exception { .routeId(Routes.SEND_EMAIL_BOP_SINGLE_PER_USER) // Clear all the headers that may come from the previous route. .removeHeaders("*") - .split(simpleF("${exchangeProperty.%s}", ExchangeProperty.FILTERED_USERNAMES)) + .split(simpleF("${exchangeProperty.%s}", ExchangeProperty.FILTERED_USERS)) .setProperty(ExchangeProperty.SINGLE_EMAIL_PER_USER, constant(true)) .to(direct(Routes.SEND_EMAIL_BOP)) .end(); diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/aggregation/UserAggregationStrategy.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/aggregation/UserAggregationStrategy.java index 6e6be6ed0a..354a5b34ed 100644 --- a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/aggregation/UserAggregationStrategy.java +++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/aggregation/UserAggregationStrategy.java @@ -1,6 +1,7 @@ package com.redhat.cloud.notifications.connector.email.aggregation; import com.redhat.cloud.notifications.connector.email.constants.ExchangeProperty; +import com.redhat.cloud.notifications.connector.email.model.settings.User; import jakarta.enterprise.context.ApplicationScoped; import org.apache.camel.AggregationStrategy; import org.apache.camel.Exchange; @@ -24,8 +25,8 @@ public Exchange aggregate(final Exchange oldExchange, final Exchange newExchange return newExchange; } - final Set oldFilteredUsers = oldExchange.getProperty(ExchangeProperty.FILTERED_USERNAMES, Set.class); - final Set newFilteredUsers = newExchange.getProperty(ExchangeProperty.FILTERED_USERNAMES, Set.class); + final Set oldFilteredUsers = oldExchange.getProperty(ExchangeProperty.FILTERED_USERS, Set.class); + final Set newFilteredUsers = newExchange.getProperty(ExchangeProperty.FILTERED_USERS, Set.class); if (newFilteredUsers != null) { oldFilteredUsers.addAll(newFilteredUsers); diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/config/EmailConnectorConfig.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/config/EmailConnectorConfig.java index 21c9422601..5161c9a364 100644 --- a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/config/EmailConnectorConfig.java +++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/config/EmailConnectorConfig.java @@ -27,6 +27,7 @@ public class EmailConnectorConfig extends ConnectorConfig { private static final String RBAC_APPLICATION_KEY = "notifications.connector.user-provider.rbac.application-key"; private static final String RBAC_ELEMENTS_PAGE = "notifications.connector.user-provider.rbac.elements-per-page"; private static final String RBAC_URL = "notifications.connector.user-provider.rbac.url"; + private static final String SKIP_BOP_USERS_RESOLUTION = "notifications.connector.bop.skip-users-resolution"; @Deprecated(forRemoval = true) public static final String SINGLE_EMAIL_PER_USER = "notifications.connector.single-email-per-user.enabled"; @@ -97,6 +98,9 @@ public class EmailConnectorConfig extends ConnectorConfig { @ConfigProperty(name = USER_PROVIDER_CACHE_EXPIRE_AFTER_WRITE, defaultValue = "600") int userProviderCacheExpireAfterWrite; + @ConfigProperty(name = SKIP_BOP_USERS_RESOLUTION, defaultValue = "false") + boolean skipBopUsersResolution; + @Override public void log() { final Map additionalEntries = new HashMap<>(); @@ -115,6 +119,7 @@ public void log() { additionalEntries.put(RBAC_URL, this.rbacURL); additionalEntries.put(SINGLE_EMAIL_PER_USER, this.singleEmailPerUserEnabled); additionalEntries.put(USER_PROVIDER_CACHE_EXPIRE_AFTER_WRITE, this.userProviderCacheExpireAfterWrite); + additionalEntries.put(SKIP_BOP_USERS_RESOLUTION, skipBopUsersResolution); log(additionalEntries); } @@ -219,4 +224,12 @@ public boolean isSingleEmailPerUserEnabled() { public int getUserProviderCacheExpireAfterWrite() { return userProviderCacheExpireAfterWrite; } + + public boolean isSkipBopUsersResolution() { + return skipBopUsersResolution; + } + + public void setSkipBopUsersResolution(boolean skipBopUsersResolution) { + this.skipBopUsersResolution = skipBopUsersResolution; + } } diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/constants/ExchangeProperty.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/constants/ExchangeProperty.java index 67212bbf52..c11f3ffee3 100644 --- a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/constants/ExchangeProperty.java +++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/constants/ExchangeProperty.java @@ -12,10 +12,10 @@ public class ExchangeProperty { */ public static final String ELEMENTS_COUNT = "elements_count"; /** - * Holds the filtered usernames. It is used in order to avoid the set of - * cached usernames from being modified. + * Holds the filtered users. It is used in order to avoid the set of + * cached users from being modified. */ - public static final String FILTERED_USERNAMES = "usernames_filtered"; + public static final String FILTERED_USERS = "users_filtered"; /** * Used to hold the received RBAC group's UUID. */ @@ -52,8 +52,7 @@ public class ExchangeProperty { public static final String RENDERED_SUBJECT = "rendered_subject"; /** * Represents the curated set of recipients that will end up receiving the - * notification through email. Since only usernames are required in order - * to send the emails, we will only grab those. + * notification through email. */ - public static final String USERNAMES = "usernames"; + public static final String USERS = "users"; } diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/model/bop/Emails.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/model/bop/Emails.java index 5767da6305..a5ee275f40 100644 --- a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/model/bop/Emails.java +++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/model/bop/Emails.java @@ -8,6 +8,7 @@ /** * Represents the payload to be sent to BOP/MBOP. */ +@Deprecated(forRemoval = true) public class Emails { @JsonProperty("emails") diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/model/bop/SendEmailsRequest.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/model/bop/SendEmailsRequest.java new file mode 100644 index 0000000000..b1a60fd3db --- /dev/null +++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/model/bop/SendEmailsRequest.java @@ -0,0 +1,19 @@ +package com.redhat.cloud.notifications.connector.email.model.bop; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; + +import java.util.HashSet; +import java.util.Set; + +import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; + +@JsonAutoDetect(fieldVisibility = ANY) +public class SendEmailsRequest { + + private final Set emails = new HashSet<>(); + private final boolean skipUsersResolution = true; + + public void addEmail(Email email) { + emails.add(email); + } +} diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/model/settings/User.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/model/settings/User.java new file mode 100644 index 0000000000..155de1de92 --- /dev/null +++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/model/settings/User.java @@ -0,0 +1,62 @@ +package com.redhat.cloud.notifications.connector.email.model.settings; + +import java.util.Objects; + +public class User { + + private String id; + private String username; + private String email; + private boolean admin; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public boolean isAdmin() { + return admin; + } + + public void setAdmin(boolean admin) { + this.admin = admin; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof User)) { + return false; + } + + User user = (User) o; + return Objects.equals(username, user.username); + } + + @Override + public int hashCode() { + return Objects.hash(username); + } +} diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/bop/BOPRequestPreparer.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/bop/BOPRequestPreparer.java index e7db8b6e37..019d33a94e 100644 --- a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/bop/BOPRequestPreparer.java +++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/bop/BOPRequestPreparer.java @@ -4,6 +4,8 @@ import com.redhat.cloud.notifications.connector.email.constants.ExchangeProperty; import com.redhat.cloud.notifications.connector.email.model.bop.Email; import com.redhat.cloud.notifications.connector.email.model.bop.Emails; +import com.redhat.cloud.notifications.connector.email.model.bop.SendEmailsRequest; +import com.redhat.cloud.notifications.connector.email.model.settings.User; import io.vertx.core.json.JsonObject; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -11,9 +13,11 @@ import org.apache.camel.Processor; import org.apache.camel.component.http.HttpMethods; -import java.util.HashSet; +import java.util.Collections; import java.util.Set; +import static java.util.stream.Collectors.toSet; + @ApplicationScoped public class BOPRequestPreparer implements Processor { @Inject @@ -27,18 +31,28 @@ public class BOPRequestPreparer implements Processor { public void process(final Exchange exchange) { final String subject = exchange.getProperty(ExchangeProperty.RENDERED_SUBJECT, String.class); final String body = exchange.getProperty(ExchangeProperty.RENDERED_BODY, String.class); - final Set recipients = new HashSet<>(); + final Set recipients; // We still need to support sending individual emails per user for a // while. However, that will go away soon, so we can consider the // following code block very much deprecated. final Boolean singleEmailPerUser = exchange.getProperty(ExchangeProperty.SINGLE_EMAIL_PER_USER, Boolean.class); if (singleEmailPerUser != null && singleEmailPerUser) { - recipients.add(exchange.getMessage().getBody(String.class)); + User recipient = exchange.getMessage().getBody(User.class); + if (emailConnectorConfig.isSkipBopUsersResolution()) { + recipients = Collections.singleton(recipient.getEmail()); + } else { + recipients = Collections.singleton(recipient.getUsername()); + } } else { - final Set usernames = exchange.getProperty(ExchangeProperty.FILTERED_USERNAMES, Set.class); - - recipients.addAll(usernames); + final Set users = exchange.getProperty(ExchangeProperty.FILTERED_USERS, Set.class); + recipients = users.stream().map(user -> { + if (emailConnectorConfig.isSkipBopUsersResolution()) { + return user.getEmail(); + } else { + return user.getUsername(); + } + }).collect(toSet()); } final Email email = new Email( @@ -47,11 +61,19 @@ public void process(final Exchange exchange) { recipients ); - final Emails emails = new Emails(); - emails.addEmail(email); + JsonObject bopBody; + if (emailConnectorConfig.isSkipBopUsersResolution()) { + final SendEmailsRequest request = new SendEmailsRequest(); + request.addEmail(email); + bopBody = JsonObject.mapFrom(request); + } else { + final Emails emails = new Emails(); + emails.addEmail(email); + bopBody = JsonObject.mapFrom(emails); + } // Specify the message's payload in JSON. - exchange.getMessage().setBody(JsonObject.mapFrom(emails).encode()); + exchange.getMessage().setBody(bopBody.encode()); // Specify the request's method. exchange.getMessage().setHeader(Exchange.HTTP_METHOD, HttpMethods.POST); diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/it/ITResponseProcessor.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/it/ITResponseProcessor.java index 9ede1ad574..6669040e60 100644 --- a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/it/ITResponseProcessor.java +++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/it/ITResponseProcessor.java @@ -3,7 +3,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.redhat.cloud.notifications.connector.email.constants.ExchangeProperty; +import com.redhat.cloud.notifications.connector.email.model.settings.User; +import com.redhat.cloud.notifications.connector.email.processors.it.pojo.response.AccountRelationship; +import com.redhat.cloud.notifications.connector.email.processors.it.pojo.response.Email; import com.redhat.cloud.notifications.connector.email.processors.it.pojo.response.ITUserResponse; +import com.redhat.cloud.notifications.connector.email.processors.it.pojo.response.Permission; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.apache.camel.Exchange; @@ -13,6 +17,8 @@ import java.util.List; import java.util.Set; +import static com.redhat.cloud.notifications.connector.email.processors.it.ITConstants.ORG_ADMIN_PERMISSION; + @ApplicationScoped public class ITResponseProcessor implements Processor { @@ -20,7 +26,7 @@ public class ITResponseProcessor implements Processor { ObjectMapper objectMapper; /** - * Processes the response from the IT service. Grabs the usernames from the + * Processes the response from the IT service. Grabs the users from the * response. * @param exchange the exchange of the pipeline. * @throws JsonProcessingException if the incoming payload cannot be read @@ -31,12 +37,12 @@ public void process(final Exchange exchange) throws JsonProcessingException { final String body = exchange.getMessage().getBody(String.class); final List itUserResponses = Arrays.asList(this.objectMapper.readValue(body, ITUserResponse[].class)); - // Get the list of usernames we will fill with the extracted usernames + // Get the list of users we will fill with the extracted users // from IT's response. - final Set usernames = exchange.getProperty(ExchangeProperty.USERNAMES, Set.class); + final Set users = exchange.getProperty(ExchangeProperty.USERS, Set.class); for (final ITUserResponse itUserResponse : itUserResponses) { - usernames.add(itUserResponse.authentications.get(0).principal); + users.add(toUser(itUserResponse)); } final int count = itUserResponses.size(); @@ -49,4 +55,31 @@ public void process(final Exchange exchange) throws JsonProcessingException { exchange.setProperty(ExchangeProperty.OFFSET, offset + limit); } } + + private static User toUser(ITUserResponse itUserResponse) { + + User user = new User(); + user.setId(itUserResponse.id); + user.setUsername(itUserResponse.authentications.get(0).principal); + + for (Email email : itUserResponse.accountRelationships.get(0).emails) { + if (email != null && email.isPrimary != null && email.isPrimary) { + user.setEmail(email.address); + } + } + + if (itUserResponse.accountRelationships != null) { + for (AccountRelationship accountRelationship : itUserResponse.accountRelationships) { + if (accountRelationship.permissions != null) { + for (Permission permission : accountRelationship.permissions) { + if (ORG_ADMIN_PERMISSION.equals(permission.permissionCode)) { + user.setAdmin(true); + } + } + } + } + } + + return user; + } } diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/rbac/RBACUsersProcessor.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/rbac/RBACUsersProcessor.java index 7cfe3c5f9f..089a53800f 100644 --- a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/rbac/RBACUsersProcessor.java +++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/rbac/RBACUsersProcessor.java @@ -2,6 +2,7 @@ import com.redhat.cloud.notifications.connector.email.constants.ExchangeProperty; import com.redhat.cloud.notifications.connector.email.model.settings.RecipientSettings; +import com.redhat.cloud.notifications.connector.email.model.settings.User; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import jakarta.enterprise.context.ApplicationScoped; @@ -10,11 +11,13 @@ import java.util.Set; +import static java.lang.Boolean.TRUE; + @ApplicationScoped public class RBACUsersProcessor implements Processor { /** * Processes the incoming payload from the RBAC service into a filtered - * set of usernames. In the case that the users are part of an RBAC group + * set of users. In the case that the users are part of an RBAC group * it is looked if only admins are allowed and if the user is active before * adding it to the set. * @param exchange the exchange of the pipeline. @@ -27,9 +30,9 @@ public void process(final Exchange exchange) { // Extract the data we need to process. final JsonArray data = responseBodyJson.getJsonArray("data"); - // Get the list of usernames we will fill with the extracted usernames + // Get the list of users we will fill with the extracted users // from RBAC's response. - final Set usernames = exchange.getProperty(ExchangeProperty.USERNAMES, Set.class); + final Set users = exchange.getProperty(ExchangeProperty.USERS, Set.class); // In case we are processing the RBAC users from the "get principals // from an RBAC group" call, we need to perform a few more checks @@ -53,7 +56,7 @@ public void process(final Exchange exchange) { } } - usernames.add(rbacUser.getString("username")); + users.add(toUser(rbacUser)); } // Store the number of elements that were returned from the page, so @@ -68,4 +71,12 @@ public void process(final Exchange exchange) { exchange.setProperty(ExchangeProperty.OFFSET, offset + limit); } } + + private static User toUser(JsonObject rbacUser) { + User user = new User(); + user.setUsername(rbacUser.getString("username")); + user.setEmail(rbacUser.getString("email")); + user.setAdmin(TRUE.equals(rbacUser.getBoolean("is_org_admin"))); + return user; + } } diff --git a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/recipients/RecipientsFilter.java b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/recipients/RecipientsFilter.java index 230e5f4324..42a8c4b70c 100644 --- a/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/recipients/RecipientsFilter.java +++ b/connector-email/src/main/java/com/redhat/cloud/notifications/connector/email/processors/recipients/RecipientsFilter.java @@ -2,6 +2,7 @@ import com.redhat.cloud.notifications.connector.email.constants.ExchangeProperty; import com.redhat.cloud.notifications.connector.email.model.settings.RecipientSettings; +import com.redhat.cloud.notifications.connector.email.model.settings.User; import jakarta.enterprise.context.ApplicationScoped; import org.apache.camel.Exchange; import org.apache.camel.Processor; @@ -21,27 +22,27 @@ public void process(final Exchange exchange) { // Fetch the required data to filter the users. final RecipientSettings recipientSettings = exchange.getProperty(ExchangeProperty.CURRENT_RECIPIENT_SETTINGS, RecipientSettings.class); final Set subscribers = (Set) exchange.getProperty(ExchangeProperty.SUBSCRIBERS, Set.class); - final Set usernames = exchange.getProperty(ExchangeProperty.USERNAMES, Set.class); - // Use a new hash set to hold the usernames. The "usernames" set that + final Set users = exchange.getProperty(ExchangeProperty.USERS, Set.class); + // Use a new hash set to hold the users. The "users" set that // comes from the user providers' responses is cached, but if we modify // it directly, it also affects the cached list. - final Set filteredUsernames = new HashSet<>(usernames); + final Set filteredUsers = new HashSet<>(users); // If the request settings contains a list of usernames, then the // recipients from RBAC who are not included in the request users list // are filtered out. Otherwise, the full list of recipients from RBAC // will be processed by the next step. if (recipientSettings.getUsers() != null && recipientSettings.getUsers().size() > 0) { - filteredUsernames.retainAll(recipientSettings.getUsers()); + filteredUsers.removeIf(user -> !recipientSettings.getUsers().contains(user.getUsername())); } // If the user preferences should be ignored, then we don't further // filter the recipients. Otherwise, we need to make sure that we only // send the notification to the subscribers of the event type. if (!recipientSettings.isIgnoreUserPreferences()) { - filteredUsernames.retainAll(subscribers); + filteredUsers.removeIf(user -> !subscribers.contains(user.getUsername())); } - exchange.setProperty(ExchangeProperty.FILTERED_USERNAMES, filteredUsernames); + exchange.setProperty(ExchangeProperty.FILTERED_USERS, filteredUsers); } } diff --git a/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/EmailRouteBuilderTest.java b/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/EmailRouteBuilderTest.java index a215e9265b..1d613921a8 100644 --- a/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/EmailRouteBuilderTest.java +++ b/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/EmailRouteBuilderTest.java @@ -3,6 +3,7 @@ import com.redhat.cloud.notifications.connector.email.config.EmailConnectorConfig; import com.redhat.cloud.notifications.connector.email.constants.ExchangeProperty; import com.redhat.cloud.notifications.connector.email.constants.Routes; +import com.redhat.cloud.notifications.connector.email.model.settings.User; import com.redhat.cloud.notifications.connector.email.processors.bop.ssl.BOPTrustManager; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; @@ -25,6 +26,8 @@ import java.util.List; import java.util.Set; +import static com.redhat.cloud.notifications.connector.email.TestUtils.createUsers; + @QuarkusTest @TestProfile(EmailRouteBuilderTest.class) public class EmailRouteBuilderTest extends CamelQuarkusTestSupport { @@ -60,10 +63,10 @@ void testIndividualEmailPerUser() throws Exception { a.mockEndpointsAndSkip(String.format("direct:%s", Routes.SEND_EMAIL_BOP)); }); - final Set usernames = Set.of("a", "b", "c", "d", "e"); + final Set users = createUsers("a", "b", "c", "d", "e"); final Exchange exchange = this.createExchangeWithBody(""); - exchange.setProperty(ExchangeProperty.FILTERED_USERNAMES, usernames); + exchange.setProperty(ExchangeProperty.FILTERED_USERS, users); final MockEndpoint sendEmailBopEndpoint = this.getMockEndpoint(String.format("mock:direct:%s", Routes.SEND_EMAIL_BOP), false); sendEmailBopEndpoint.expectedMessageCount(5); diff --git a/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/TestUtils.java b/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/TestUtils.java new file mode 100644 index 0000000000..59a0ce4232 --- /dev/null +++ b/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/TestUtils.java @@ -0,0 +1,24 @@ +package com.redhat.cloud.notifications.connector.email; + +import com.redhat.cloud.notifications.connector.email.model.settings.User; + +import java.util.Arrays; +import java.util.Set; + +import static java.util.stream.Collectors.toSet; + +public class TestUtils { + + public static User createUser(String username) { + User user = new User(); + user.setUsername(username); + user.setEmail(username + "-email"); + return user; + } + + public static Set createUsers(String... usernames) { + return Arrays.stream(usernames) + .map(TestUtils::createUser) + .collect(toSet()); + } +} diff --git a/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/aggregation/UserAggregationStrategyTest.java b/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/aggregation/UserAggregationStrategyTest.java index 805dee3daf..3338dd17b3 100644 --- a/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/aggregation/UserAggregationStrategyTest.java +++ b/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/aggregation/UserAggregationStrategyTest.java @@ -1,7 +1,7 @@ package com.redhat.cloud.notifications.connector.email.aggregation; import com.redhat.cloud.notifications.connector.email.constants.ExchangeProperty; -import com.redhat.cloud.notifications.connector.email.processors.recipients.RecipientsFilterTest; +import com.redhat.cloud.notifications.connector.email.model.settings.User; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import org.apache.camel.Exchange; @@ -10,9 +10,10 @@ import org.junit.jupiter.api.Test; import java.util.HashSet; -import java.util.List; import java.util.Set; +import static com.redhat.cloud.notifications.connector.email.TestUtils.createUsers; + @QuarkusTest public class UserAggregationStrategyTest extends CamelQuarkusTestSupport { @Inject @@ -25,21 +26,15 @@ public class UserAggregationStrategyTest extends CamelQuarkusTestSupport { void testAggregation() { // Prepare the list of users to simulate an incoming exchange with a // few first ones. - final Set users = new HashSet<>(); - users.add("foo"); - users.add("bar"); - users.add("baz"); + final Set users = createUsers("foo", "bar", "baz"); // Prepare the list of users to simulate a second incoming exchange // with more users. - final Set users2 = new HashSet<>(); - users2.add("johndoe"); - users2.add("janedoe"); - users2.add("jimmydoe"); + final Set users2 = createUsers("johndoe", "janedoe", "jimmydoe"); // Prepare the list of users that we are expecting to find at the end // of the test. - final Set finalExpectedUsers = new HashSet<>(); + final Set finalExpectedUsers = new HashSet<>(); finalExpectedUsers.addAll(users); finalExpectedUsers.addAll(users2); @@ -52,7 +47,7 @@ void testAggregation() { // Prepare an old exchange to simulate the aggregation process. final Exchange oldExchange = this.createExchangeWithBody(""); - oldExchange.setProperty(ExchangeProperty.FILTERED_USERNAMES, users); + oldExchange.setProperty(ExchangeProperty.FILTERED_USERS, users); // Call the aggregator under test. final Exchange resultExchangeTwo = this.userAggregationStrategy.aggregate(oldExchange, newExchange); @@ -60,20 +55,20 @@ void testAggregation() { // Check that since the new exchange didn't have any new users, the old // exchange should contain the original ones. - final List resultUsers = resultExchangeTwo.getProperty(ExchangeProperty.FILTERED_USERNAMES, List.class); - RecipientsFilterTest.assertUsernameCollectionsEqualsIgnoreOrder(users, resultUsers); + final Set resultUsers = resultExchangeTwo.getProperty(ExchangeProperty.FILTERED_USERS, Set.class); + Assertions.assertEquals(users, resultUsers); // Prepare a new exchange with a new list of users final Exchange yetAnotherExchange = this.createExchangeWithBody(""); - yetAnotherExchange.setProperty(ExchangeProperty.FILTERED_USERNAMES, users2); + yetAnotherExchange.setProperty(ExchangeProperty.FILTERED_USERS, users2); // Call the exchange under test. final Exchange finalExchange = this.userAggregationStrategy.aggregate(oldExchange, yetAnotherExchange); Assertions.assertEquals(oldExchange, finalExchange, "on following split iterations, the old exchange should be returned along with the aggregated users"); // Assert that the old exchange contains all the aggregated users. - final List finalUsersList = finalExchange.getProperty(ExchangeProperty.FILTERED_USERNAMES, List.class); - RecipientsFilterTest.assertUsernameCollectionsEqualsIgnoreOrder(finalExpectedUsers, finalUsersList); + final Set finalUsersList = finalExchange.getProperty(ExchangeProperty.FILTERED_USERS, Set.class); + Assertions.assertEquals(finalExpectedUsers, finalUsersList); } } diff --git a/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/processors/bop/BOPRequestPreparerTest.java b/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/processors/bop/BOPRequestPreparerTest.java index b1017e873a..49ee9a2c5c 100644 --- a/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/processors/bop/BOPRequestPreparerTest.java +++ b/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/processors/bop/BOPRequestPreparerTest.java @@ -3,6 +3,7 @@ import com.google.common.io.Resources; import com.redhat.cloud.notifications.connector.email.config.EmailConnectorConfig; import com.redhat.cloud.notifications.connector.email.constants.ExchangeProperty; +import com.redhat.cloud.notifications.connector.email.model.settings.User; import io.quarkus.test.junit.QuarkusTest; import io.vertx.core.json.JsonObject; import jakarta.inject.Inject; @@ -10,15 +11,18 @@ import org.apache.camel.component.http.HttpMethods; import org.apache.camel.quarkus.test.CamelQuarkusTestSupport; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.io.IOException; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.HashSet; import java.util.Map; import java.util.Set; +import static com.redhat.cloud.notifications.connector.email.TestUtils.createUser; +import static com.redhat.cloud.notifications.connector.email.TestUtils.createUsers; + @QuarkusTest public class BOPRequestPreparerTest extends CamelQuarkusTestSupport { @Inject @@ -31,41 +35,48 @@ public class BOPRequestPreparerTest extends CamelQuarkusTestSupport { * Tests that the processor prepares the request as intended. * @throws IOException if the expected results' file could not be loaded. */ - @Test - void testProcess() throws IOException { - // Prepare the properties that the processor expects. - final String emailSubject = "this is a fake subject"; - final String emailBody = "this is a fake body"; - // Manually create the hash set because the "Set.of" utility doesn't - // respect the insertion ordering. - final Set usernames = new HashSet<>(); - usernames.add("a"); - usernames.add("b"); - usernames.add("c"); - - final Exchange exchange = this.createExchangeWithBody(""); - exchange.setProperty(ExchangeProperty.RENDERED_SUBJECT, emailSubject); - exchange.setProperty(ExchangeProperty.RENDERED_BODY, emailBody); - exchange.setProperty(ExchangeProperty.FILTERED_USERNAMES, usernames); - - // Call the processor under test. - this.bopRequestPreparer.process(exchange); - - // Assert that the headers are correct. - final Map headers = exchange.getMessage().getHeaders(); - Assertions.assertEquals(HttpMethods.POST, headers.get(Exchange.HTTP_METHOD)); - Assertions.assertEquals("/v1/sendEmails", headers.get(Exchange.HTTP_PATH)); - Assertions.assertEquals("application/json", headers.get(Exchange.CONTENT_TYPE)); - Assertions.assertEquals(this.emailConnectorConfig.getBopApiToken(), headers.get(Constants.BOP_API_TOKEN_HEADER)); - Assertions.assertEquals(this.emailConnectorConfig.getBopClientId(), headers.get(Constants.BOP_CLIENT_ID_HEADER)); - Assertions.assertEquals(this.emailConnectorConfig.getBopEnv(), headers.get(Constants.BOP_ENV_HEADER)); - - // Assert that the message's body is correct. - final URL url = Resources.getResource("processors/bop/expectedBody.json"); - final String expectedBodyRaw = Resources.toString(url, StandardCharsets.UTF_8); - final String expectedBody = new JsonObject(expectedBodyRaw).encode(); - - Assertions.assertEquals(expectedBody, exchange.getMessage().getBody()); + @ValueSource(booleans = { true, false }) + @ParameterizedTest + void testProcess(boolean skipBopUsersResolution) throws IOException { + emailConnectorConfig.setSkipBopUsersResolution(skipBopUsersResolution); + + try { + // Prepare the properties that the processor expects. + final String emailSubject = "this is a fake subject"; + final String emailBody = "this is a fake body"; + final Set users = createUsers("a", "b", "c"); + + final Exchange exchange = this.createExchangeWithBody(""); + exchange.setProperty(ExchangeProperty.RENDERED_SUBJECT, emailSubject); + exchange.setProperty(ExchangeProperty.RENDERED_BODY, emailBody); + exchange.setProperty(ExchangeProperty.FILTERED_USERS, users); + + // Call the processor under test. + this.bopRequestPreparer.process(exchange); + + // Assert that the headers are correct. + final Map headers = exchange.getMessage().getHeaders(); + Assertions.assertEquals(HttpMethods.POST, headers.get(Exchange.HTTP_METHOD)); + Assertions.assertEquals("/v1/sendEmails", headers.get(Exchange.HTTP_PATH)); + Assertions.assertEquals("application/json", headers.get(Exchange.CONTENT_TYPE)); + Assertions.assertEquals(this.emailConnectorConfig.getBopApiToken(), headers.get(Constants.BOP_API_TOKEN_HEADER)); + Assertions.assertEquals(this.emailConnectorConfig.getBopClientId(), headers.get(Constants.BOP_CLIENT_ID_HEADER)); + Assertions.assertEquals(this.emailConnectorConfig.getBopEnv(), headers.get(Constants.BOP_ENV_HEADER)); + + // Assert that the message's body is correct. + URL url; + if (skipBopUsersResolution) { + url = Resources.getResource("processors/bop/expectedBodySkipUsersResolution.json"); + } else { + url = Resources.getResource("processors/bop/expectedBody.json"); + } + final String expectedBodyRaw = Resources.toString(url, StandardCharsets.UTF_8); + final String expectedBody = new JsonObject(expectedBodyRaw).encode(); + + Assertions.assertEquals(expectedBody, exchange.getMessage().getBody()); + } finally { + emailConnectorConfig.setSkipBopUsersResolution(false); + } } /** @@ -73,37 +84,49 @@ void testProcess() throws IOException { * @throws IOException if the expected results' file could not be loaded. */ @Deprecated(forRemoval = true) - @Test - void testProcessSingleEmail() throws IOException { - // Prepare the properties that the processor expects. - final String emailSubject = "this is a fake subject"; - final String emailBody = "this is a fake body"; - - // The "split" operation is going to leave the username in the - // exchange's body, so we only need to pass a simple string in this - // case. - final Exchange exchange = this.createExchangeWithBody("a"); - exchange.setProperty(ExchangeProperty.RENDERED_SUBJECT, emailSubject); - exchange.setProperty(ExchangeProperty.RENDERED_BODY, emailBody); - exchange.setProperty(ExchangeProperty.SINGLE_EMAIL_PER_USER, true); - - // Call the processor under test. - this.bopRequestPreparer.process(exchange); - - // Assert that the headers are correct. - final Map headers = exchange.getMessage().getHeaders(); - Assertions.assertEquals(HttpMethods.POST, headers.get(Exchange.HTTP_METHOD)); - Assertions.assertEquals("/v1/sendEmails", headers.get(Exchange.HTTP_PATH)); - Assertions.assertEquals("application/json", headers.get(Exchange.CONTENT_TYPE)); - Assertions.assertEquals(this.emailConnectorConfig.getBopApiToken(), headers.get(Constants.BOP_API_TOKEN_HEADER)); - Assertions.assertEquals(this.emailConnectorConfig.getBopClientId(), headers.get(Constants.BOP_CLIENT_ID_HEADER)); - Assertions.assertEquals(this.emailConnectorConfig.getBopEnv(), headers.get(Constants.BOP_ENV_HEADER)); - - // Assert that the message's body is correct. - final URL url = Resources.getResource("processors/bop/expectedBodySingleUser.json"); - final String expectedBodyRaw = Resources.toString(url, StandardCharsets.UTF_8); - final String expectedBody = new JsonObject(expectedBodyRaw).encode(); - - Assertions.assertEquals(expectedBody, exchange.getMessage().getBody()); + @ValueSource(booleans = { true, false }) + @ParameterizedTest + void testProcessSingleEmail(boolean skipBopUsersResolution) throws IOException { + emailConnectorConfig.setSkipBopUsersResolution(skipBopUsersResolution); + + try { + // Prepare the properties that the processor expects. + final String emailSubject = "this is a fake subject"; + final String emailBody = "this is a fake body"; + + // The "split" operation is going to leave the user in the + // exchange's body, so we only need to pass a simple string in this + // case. + final Exchange exchange = this.createExchangeWithBody(createUser("a")); + exchange.setProperty(ExchangeProperty.RENDERED_SUBJECT, emailSubject); + exchange.setProperty(ExchangeProperty.RENDERED_BODY, emailBody); + exchange.setProperty(ExchangeProperty.SINGLE_EMAIL_PER_USER, true); + + // Call the processor under test. + this.bopRequestPreparer.process(exchange); + + // Assert that the headers are correct. + final Map headers = exchange.getMessage().getHeaders(); + Assertions.assertEquals(HttpMethods.POST, headers.get(Exchange.HTTP_METHOD)); + Assertions.assertEquals("/v1/sendEmails", headers.get(Exchange.HTTP_PATH)); + Assertions.assertEquals("application/json", headers.get(Exchange.CONTENT_TYPE)); + Assertions.assertEquals(this.emailConnectorConfig.getBopApiToken(), headers.get(Constants.BOP_API_TOKEN_HEADER)); + Assertions.assertEquals(this.emailConnectorConfig.getBopClientId(), headers.get(Constants.BOP_CLIENT_ID_HEADER)); + Assertions.assertEquals(this.emailConnectorConfig.getBopEnv(), headers.get(Constants.BOP_ENV_HEADER)); + + // Assert that the message's body is correct. + URL url; + if (emailConnectorConfig.isSkipBopUsersResolution()) { + url = Resources.getResource("processors/bop/expectedBodySingleUserSkipUsersResolution.json"); + } else { + url = Resources.getResource("processors/bop/expectedBodySingleUser.json"); + } + final String expectedBodyRaw = Resources.toString(url, StandardCharsets.UTF_8); + final String expectedBody = new JsonObject(expectedBodyRaw).encode(); + + Assertions.assertEquals(expectedBody, exchange.getMessage().getBody()); + } finally { + emailConnectorConfig.setSkipBopUsersResolution(false); + } } } diff --git a/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/processors/it/ITResponseProcessorTest.java b/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/processors/it/ITResponseProcessorTest.java index fa92d83f59..82d239baba 100644 --- a/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/processors/it/ITResponseProcessorTest.java +++ b/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/processors/it/ITResponseProcessorTest.java @@ -3,6 +3,7 @@ import com.google.common.io.Resources; import com.redhat.cloud.notifications.connector.email.config.EmailConnectorConfig; import com.redhat.cloud.notifications.connector.email.constants.ExchangeProperty; +import com.redhat.cloud.notifications.connector.email.model.settings.User; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import org.apache.camel.Exchange; @@ -44,15 +45,15 @@ void testProcessLessThanLimit() throws IOException { final Exchange exchange = this.createExchangeWithBody(incomingBody); exchange.setProperty(ExchangeProperty.LIMIT, limit); exchange.setProperty(ExchangeProperty.OFFSET, offset); - exchange.setProperty(ExchangeProperty.USERNAMES, new HashSet()); + exchange.setProperty(ExchangeProperty.USERS, new HashSet()); // Call the processor under test. this.itResponseProcessor.process(exchange); // Assert that the grabbed username is correct. - final Set usernames = exchange.getProperty(ExchangeProperty.USERNAMES, Set.class); - Assertions.assertEquals(1, usernames.size()); - Assertions.assertEquals("foo", usernames.iterator().next()); + final Set users = exchange.getProperty(ExchangeProperty.USERS, Set.class); + Assertions.assertEquals(1, users.size()); + Assertions.assertEquals("foo", users.iterator().next().getUsername()); // Assert that only the expected element was read. Assertions.assertEquals(1, exchange.getProperty(ExchangeProperty.ELEMENTS_COUNT)); @@ -82,15 +83,15 @@ void testProcessExactAsLimit() throws IOException { final Exchange exchange = this.createExchangeWithBody(incomingBody); exchange.setProperty(ExchangeProperty.LIMIT, limit); exchange.setProperty(ExchangeProperty.OFFSET, offset); - exchange.setProperty(ExchangeProperty.USERNAMES, new HashSet()); + exchange.setProperty(ExchangeProperty.USERS, new HashSet()); // Call the processor under test. this.itResponseProcessor.process(exchange); // Assert that the grabbed username is correct. - final Set usernames = exchange.getProperty(ExchangeProperty.USERNAMES, Set.class); - Assertions.assertEquals(1, usernames.size()); - Assertions.assertEquals("foo", usernames.iterator().next()); + final Set users = exchange.getProperty(ExchangeProperty.USERS, Set.class); + Assertions.assertEquals(1, users.size()); + Assertions.assertEquals("foo", users.iterator().next().getUsername()); // Assert that only the expected element was read. Assertions.assertEquals(1, exchange.getProperty(ExchangeProperty.ELEMENTS_COUNT)); diff --git a/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/processors/rbac/RBACUsersProcessorTest.java b/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/processors/rbac/RBACUsersProcessorTest.java index 97c86dfa78..c9baf6b432 100644 --- a/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/processors/rbac/RBACUsersProcessorTest.java +++ b/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/processors/rbac/RBACUsersProcessorTest.java @@ -4,6 +4,7 @@ import com.redhat.cloud.notifications.connector.email.config.EmailConnectorConfig; import com.redhat.cloud.notifications.connector.email.constants.ExchangeProperty; import com.redhat.cloud.notifications.connector.email.model.settings.RecipientSettings; +import com.redhat.cloud.notifications.connector.email.model.settings.User; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import org.apache.camel.Exchange; @@ -18,6 +19,8 @@ import java.util.Set; import java.util.UUID; +import static com.redhat.cloud.notifications.connector.email.TestUtils.createUsers; + @QuarkusTest public class RBACUsersProcessorTest extends CamelQuarkusTestSupport { @Inject @@ -47,7 +50,7 @@ void testProcessRegularCall() throws IOException { ); exchange.setProperty(ExchangeProperty.CURRENT_RECIPIENT_SETTINGS, recipientSettings); exchange.setProperty(ExchangeProperty.LIMIT, this.emailConnectorConfig.getRbacElementsPerPage()); - exchange.setProperty(ExchangeProperty.USERNAMES, new HashSet()); + exchange.setProperty(ExchangeProperty.USERS, new HashSet()); // Call the processor under test. this.rbacUsersProcessor.process(exchange); @@ -55,16 +58,11 @@ void testProcessRegularCall() throws IOException { // Assert that the elements count is correct. Assertions.assertEquals(5, exchange.getProperty(ExchangeProperty.ELEMENTS_COUNT)); - // Assert that the usernames are the expected ones. - final Set expectedUsernames = new HashSet<>(); - expectedUsernames.add("foouser"); - expectedUsernames.add("baruser"); - expectedUsernames.add("bazuser"); - expectedUsernames.add("johndoe"); - expectedUsernames.add("janedoe"); + // Assert that the users are the expected ones. + final Set expectedUsers = createUsers("foouser", "baruser", "bazuser", "johndoe", "janedoe"); - final Set result = exchange.getProperty(ExchangeProperty.USERNAMES, Set.class); - Assertions.assertIterableEquals(expectedUsernames, result); + final Set result = exchange.getProperty(ExchangeProperty.USERS, Set.class); + Assertions.assertEquals(expectedUsers, result); } /** @@ -94,7 +92,7 @@ void testProcessRegularCallCountMatchesLimit() throws IOException { final int limit = 5; exchange.setProperty(ExchangeProperty.OFFSET, offset); exchange.setProperty(ExchangeProperty.LIMIT, limit); - exchange.setProperty(ExchangeProperty.USERNAMES, new HashSet()); + exchange.setProperty(ExchangeProperty.USERS, new HashSet()); // Call the processor under test. this.rbacUsersProcessor.process(exchange); @@ -102,16 +100,11 @@ void testProcessRegularCallCountMatchesLimit() throws IOException { // Assert that the elements count is correct. Assertions.assertEquals(5, exchange.getProperty(ExchangeProperty.ELEMENTS_COUNT)); - // Assert that the usernames are the expected ones. - final Set expectedUsernames = new HashSet<>(); - expectedUsernames.add("foouser"); - expectedUsernames.add("baruser"); - expectedUsernames.add("bazuser"); - expectedUsernames.add("johndoe"); - expectedUsernames.add("janedoe"); + // Assert that the users are the expected ones. + final Set expectedUsers = createUsers("foouser", "baruser", "bazuser", "johndoe", "janedoe"); - final Set result = exchange.getProperty(ExchangeProperty.USERNAMES, Set.class); - Assertions.assertIterableEquals(expectedUsernames, result); + final Set result = exchange.getProperty(ExchangeProperty.USERS, Set.class); + Assertions.assertEquals(expectedUsers, result); // Assert that the offset got updated. Assertions.assertEquals(offset + limit, exchange.getProperty(ExchangeProperty.OFFSET)); @@ -140,7 +133,7 @@ void testProcessGroupCall() throws IOException { ); exchange.setProperty(ExchangeProperty.CURRENT_RECIPIENT_SETTINGS, recipientSettings); exchange.setProperty(ExchangeProperty.LIMIT, this.emailConnectorConfig.getRbacElementsPerPage()); - exchange.setProperty(ExchangeProperty.USERNAMES, new HashSet()); + exchange.setProperty(ExchangeProperty.USERS, new HashSet()); // Call the processor under test. this.rbacUsersProcessor.process(exchange); @@ -148,12 +141,10 @@ void testProcessGroupCall() throws IOException { // Assert that the elements count is correct. Assertions.assertEquals(5, exchange.getProperty(ExchangeProperty.ELEMENTS_COUNT)); - // Assert that the usernames are the expected ones. - final Set expectedUsernames = new HashSet<>(); - expectedUsernames.add("foouser"); - expectedUsernames.add("baruser"); + // Assert that the users are the expected ones. + final Set expectedUsers = createUsers("foouser", "baruser"); - final Set result = exchange.getProperty(ExchangeProperty.USERNAMES, Set.class); - Assertions.assertIterableEquals(expectedUsernames, result); + final Set result = exchange.getProperty(ExchangeProperty.USERS, Set.class); + Assertions.assertIterableEquals(expectedUsers, result); } } diff --git a/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/processors/recipients/RecipientsFilterTest.java b/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/processors/recipients/RecipientsFilterTest.java index dd779ea494..d3ad17a31d 100644 --- a/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/processors/recipients/RecipientsFilterTest.java +++ b/connector-email/src/test/java/com/redhat/cloud/notifications/connector/email/processors/recipients/RecipientsFilterTest.java @@ -2,6 +2,7 @@ import com.redhat.cloud.notifications.connector.email.constants.ExchangeProperty; import com.redhat.cloud.notifications.connector.email.model.settings.RecipientSettings; +import com.redhat.cloud.notifications.connector.email.model.settings.User; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import org.apache.camel.Exchange; @@ -10,10 +11,11 @@ import org.junit.jupiter.api.Test; import java.util.Collection; -import java.util.HashSet; import java.util.Set; import java.util.UUID; +import static com.redhat.cloud.notifications.connector.email.TestUtils.createUsers; + @QuarkusTest public class RecipientsFilterTest extends CamelQuarkusTestSupport { @@ -31,13 +33,7 @@ public class RecipientsFilterTest extends CamelQuarkusTestSupport { void testRecipientSettingsUsersKept() { final Set recipientUsers = Set.of("a", "c", "e"); final Set subscribers = Set.of("b"); - // The set needs to be defined this way in order for it to be modified. - final Set usernames = new HashSet<>(); - usernames.add("a"); - usernames.add("b"); - usernames.add("c"); - usernames.add("d"); - usernames.add("e"); + final Set users = createUsers("a", "b", "c", "d", "e"); final RecipientSettings recipientSettings = new RecipientSettings( true, @@ -52,21 +48,16 @@ void testRecipientSettingsUsersKept() { final Exchange exchange = this.createExchangeWithBody(""); exchange.setProperty(ExchangeProperty.CURRENT_RECIPIENT_SETTINGS, recipientSettings); exchange.setProperty(ExchangeProperty.SUBSCRIBERS, subscribers); - exchange.setProperty(ExchangeProperty.USERNAMES, usernames); + exchange.setProperty(ExchangeProperty.USERS, users); // Call the processor under test. this.recipientsFilter.process(exchange); - // Assert that the list contains the expected elements. The - // "expectedResult" set is manually created because "Set.of" doesn't - // respect the order of the specified elements. - final Set expectedResult = new HashSet<>(); - expectedResult.add("a"); - expectedResult.add("c"); - expectedResult.add("e"); - final Set result = exchange.getProperty(ExchangeProperty.FILTERED_USERNAMES, Set.class); + // Assert that the list contains the expected elements. + final Set expectedResult = createUsers("a", "c", "e"); + final Set result = exchange.getProperty(ExchangeProperty.FILTERED_USERS, Set.class); - assertUsernameCollectionsEqualsIgnoreOrder(expectedResult, result); + Assertions.assertEquals(expectedResult, result); } /** @@ -76,8 +67,7 @@ void testRecipientSettingsUsersKept() { @Test void testIgnoreUserPreferences() { final Set subscribers = Set.of("b"); - // The set needs to be defined this way in order for it to be modified. - final Set usernames = Set.of("a", "b", "c", "d", "e"); + final Set users = createUsers("a", "b", "c", "d", "e"); final RecipientSettings recipientSettings = new RecipientSettings( true, @@ -92,15 +82,15 @@ void testIgnoreUserPreferences() { final Exchange exchange = this.createExchangeWithBody(""); exchange.setProperty(ExchangeProperty.CURRENT_RECIPIENT_SETTINGS, recipientSettings); exchange.setProperty(ExchangeProperty.SUBSCRIBERS, subscribers); - exchange.setProperty(ExchangeProperty.USERNAMES, usernames); + exchange.setProperty(ExchangeProperty.USERS, users); // Call the processor under test. this.recipientsFilter.process(exchange); // Assert that the list contains the expected elements. - final Set result = exchange.getProperty(ExchangeProperty.FILTERED_USERNAMES, Set.class); + final Set result = exchange.getProperty(ExchangeProperty.FILTERED_USERS, Set.class); - assertUsernameCollectionsEqualsIgnoreOrder(usernames, result); + Assertions.assertEquals(users, result); } /** @@ -110,13 +100,7 @@ void testIgnoreUserPreferences() { @Test void testFilterUnsubscribedUsernames() { final Set subscribers = Set.of("b", "c", "e"); - // The set needs to be defined this way in order for it to be modified. - final Set usernames = new HashSet<>(); - usernames.add("a"); - usernames.add("b"); - usernames.add("c"); - usernames.add("d"); - usernames.add("e"); + final Set users = createUsers("a", "b", "c", "d", "e"); final RecipientSettings recipientSettings = new RecipientSettings( true, @@ -132,7 +116,7 @@ void testFilterUnsubscribedUsernames() { final Exchange exchange = this.createExchangeWithBody(""); exchange.setProperty(ExchangeProperty.CURRENT_RECIPIENT_SETTINGS, recipientSettings); exchange.setProperty(ExchangeProperty.SUBSCRIBERS, subscribers); - exchange.setProperty(ExchangeProperty.USERNAMES, usernames); + exchange.setProperty(ExchangeProperty.USERS, users); // Call the processor under test. this.recipientsFilter.process(exchange); @@ -140,13 +124,10 @@ void testFilterUnsubscribedUsernames() { // Assert that the list contains the expected elements. The // "expectedResult" set is manually created because "Set.of" doesn't // respect the order of the specified elements. - final Set expectedResult = new HashSet<>(); - expectedResult.add("b"); - expectedResult.add("c"); - expectedResult.add("e"); - final Set result = exchange.getProperty(ExchangeProperty.FILTERED_USERNAMES, Set.class); + final Set expectedResult = createUsers("b", "c", "e"); + final Set result = exchange.getProperty(ExchangeProperty.FILTERED_USERS, Set.class); - assertUsernameCollectionsEqualsIgnoreOrder(expectedResult, result); + Assertions.assertEquals(expectedResult, result); } /** diff --git a/connector-email/src/test/resources/processors/bop/expectedBodySingleUserSkipUsersResolution.json b/connector-email/src/test/resources/processors/bop/expectedBodySingleUserSkipUsersResolution.json new file mode 100644 index 0000000000..2aa18b7900 --- /dev/null +++ b/connector-email/src/test/resources/processors/bop/expectedBodySingleUserSkipUsersResolution.json @@ -0,0 +1,14 @@ +{ + "emails": + [ + { + "subject":"this is a fake subject", + "body":"this is a fake body", + "recipients":[], + "ccList":[], + "bccList":["a-email"], + "bodyType":"html" + } + ], + "skipUsersResolution": true +} diff --git a/connector-email/src/test/resources/processors/bop/expectedBodySkipUsersResolution.json b/connector-email/src/test/resources/processors/bop/expectedBodySkipUsersResolution.json new file mode 100644 index 0000000000..48c6067eb6 --- /dev/null +++ b/connector-email/src/test/resources/processors/bop/expectedBodySkipUsersResolution.json @@ -0,0 +1,14 @@ +{ + "emails": + [ + { + "subject":"this is a fake subject", + "body":"this is a fake body", + "recipients":[], + "ccList":[], + "bccList":["a-email","b-email","c-email"], + "bodyType":"html" + } + ], + "skipUsersResolution": true +} diff --git a/engine/src/main/java/com/redhat/cloud/notifications/processors/email/EmailSender.java b/engine/src/main/java/com/redhat/cloud/notifications/processors/email/EmailSender.java index ad3af78de7..42bb8d6500 100644 --- a/engine/src/main/java/com/redhat/cloud/notifications/processors/email/EmailSender.java +++ b/engine/src/main/java/com/redhat/cloud/notifications/processors/email/EmailSender.java @@ -188,13 +188,23 @@ private JsonObject getPayload(Set users, EventWrapper eventWrapper, ); throw e; } - Emails emails = new Emails(); - emails.addEmail(buildEmail( - users, - renderedSubject, - renderedBody - )); - return JsonObject.mapFrom(emails); + if (featureFlipper.isSkipBopUsersResolution()) { + SendEmailsRequest request = new SendEmailsRequest(); + request.addEmail(buildEmail( + users, + renderedSubject, + renderedBody + )); + return JsonObject.mapFrom(request); + } else { + Emails emails = new Emails(); + emails.addEmail(buildEmail( + users, + renderedSubject, + renderedBody + )); + return JsonObject.mapFrom(emails); + } } @Deprecated(forRemoval = true) @@ -212,13 +222,23 @@ private JsonObject getPayload(User user, EventWrapper eventWrapper, Templa ); throw e; } - Emails emails = new Emails(); - emails.addEmail(buildEmail( - user.getUsername(), - renderedSubject, - renderedBody - )); - return JsonObject.mapFrom(emails); + if (featureFlipper.isSkipBopUsersResolution()) { + SendEmailsRequest request = new SendEmailsRequest(); + request.addEmail(buildEmail( + user.getEmail(), + renderedSubject, + renderedBody + )); + return JsonObject.mapFrom(request); + } else { + Emails emails = new Emails(); + emails.addEmail(buildEmail( + user.getUsername(), + renderedSubject, + renderedBody + )); + return JsonObject.mapFrom(emails); + } } protected HttpRequest buildBOPHttpRequest() { @@ -240,7 +260,13 @@ protected Email buildEmail(String recipient, String subject, String body) { } protected Email buildEmail(Set recipients, String subject, String body) { - Set usersEmail = recipients.stream().map(User::getUsername).collect(Collectors.toSet()); + Set usersEmail; + if (featureFlipper.isSkipBopUsersResolution()) { + usersEmail = recipients.stream().map(User::getEmail).collect(Collectors.toSet()); + } else { + usersEmail = recipients.stream().map(User::getUsername).collect(Collectors.toSet()); + } + Email email = new Email(); email.setBodyType(BODY_TYPE_HTML); if (featureFlipper.isAddDefaultRecipientOnSingleEmail()) { diff --git a/engine/src/main/java/com/redhat/cloud/notifications/processors/email/Emails.java b/engine/src/main/java/com/redhat/cloud/notifications/processors/email/Emails.java index be8fbcf3fe..b1293e936c 100644 --- a/engine/src/main/java/com/redhat/cloud/notifications/processors/email/Emails.java +++ b/engine/src/main/java/com/redhat/cloud/notifications/processors/email/Emails.java @@ -5,6 +5,7 @@ import java.util.HashSet; import java.util.Set; +@Deprecated(forRemoval = true) public class Emails { @JsonProperty("emails") diff --git a/engine/src/main/java/com/redhat/cloud/notifications/processors/email/SendEmailsRequest.java b/engine/src/main/java/com/redhat/cloud/notifications/processors/email/SendEmailsRequest.java new file mode 100644 index 0000000000..3efa4559df --- /dev/null +++ b/engine/src/main/java/com/redhat/cloud/notifications/processors/email/SendEmailsRequest.java @@ -0,0 +1,19 @@ +package com.redhat.cloud.notifications.processors.email; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; + +import java.util.HashSet; +import java.util.Set; + +import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; + +@JsonAutoDetect(fieldVisibility = ANY) +public class SendEmailsRequest { + + private final Set emails = new HashSet<>(); + private final boolean skipUsersResolution = true; + + public void addEmail(Email email) { + emails.add(email); + } +} diff --git a/recipients-resolver/src/main/java/com/redhat/cloud/notifications/recipients/model/User.java b/recipients-resolver/src/main/java/com/redhat/cloud/notifications/recipients/model/User.java index a765bce957..5174ff03cf 100644 --- a/recipients-resolver/src/main/java/com/redhat/cloud/notifications/recipients/model/User.java +++ b/recipients-resolver/src/main/java/com/redhat/cloud/notifications/recipients/model/User.java @@ -6,6 +6,7 @@ public class User { private String id; private String username; + private String email; private boolean admin; public String getId() { @@ -24,6 +25,14 @@ public void setUsername(String username) { this.username = username; } + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + public boolean isAdmin() { return admin; } diff --git a/recipients-resolver/src/main/java/com/redhat/cloud/notifications/recipients/resolver/FetchUsersFromExternalServices.java b/recipients-resolver/src/main/java/com/redhat/cloud/notifications/recipients/resolver/FetchUsersFromExternalServices.java index 4fab774e62..0479399cb5 100644 --- a/recipients-resolver/src/main/java/com/redhat/cloud/notifications/recipients/resolver/FetchUsersFromExternalServices.java +++ b/recipients-resolver/src/main/java/com/redhat/cloud/notifications/recipients/resolver/FetchUsersFromExternalServices.java @@ -5,6 +5,7 @@ import com.redhat.cloud.notifications.recipients.resolver.itservice.ITUserService; import com.redhat.cloud.notifications.recipients.resolver.itservice.pojo.request.ITUserRequest; import com.redhat.cloud.notifications.recipients.resolver.itservice.pojo.response.AccountRelationship; +import com.redhat.cloud.notifications.recipients.resolver.itservice.pojo.response.Email; import com.redhat.cloud.notifications.recipients.resolver.itservice.pojo.response.ITUserResponse; import com.redhat.cloud.notifications.recipients.resolver.itservice.pojo.response.Permission; import com.redhat.cloud.notifications.recipients.resolver.mbop.MBOPService; @@ -177,6 +178,12 @@ List transformItUserToUser(List itUserResponses) { user.setId(itUserResponse.id); user.setUsername(itUserResponse.authentications.get(0).principal); + for (Email email : itUserResponse.accountRelationships.get(0).emails) { + if (email != null && email.isPrimary != null && email.isPrimary) { + user.setEmail(email.address); + } + } + if (itUserResponse.accountRelationships != null) { for (AccountRelationship accountRelationship : itUserResponse.accountRelationships) { if (accountRelationship.permissions != null) { @@ -255,6 +262,7 @@ private List getWithPagination(Function> fetcher) if (rbacUser.getActive()) { User user = new User(); user.setUsername(rbacUser.getUsername()); + user.setEmail(rbacUser.getEmail()); user.setAdmin(TRUE.equals(rbacUser.getOrgAdmin())); users.add(user); } @@ -272,6 +280,7 @@ List transformMBOPUserToUser(final List mbopUsers) { user.setId(mbopUser.id()); user.setUsername(mbopUser.username()); + user.setEmail(mbopUser.email()); user.setAdmin(mbopUser.isOrgAdmin()); users.add(user);