Skip to content

Commit

Permalink
fix: prevent emails to be sent to non opted in user in trial instance
Browse files Browse the repository at this point in the history
  • Loading branch information
ytvnr committed Mar 27, 2024
1 parent f7014d3 commit 6a6c2d1
Show file tree
Hide file tree
Showing 18 changed files with 1,551 additions and 297 deletions.
5 changes: 5 additions & 0 deletions gravitee-apim-console-webui/src/entities/consoleSettings.ts
Expand Up @@ -30,6 +30,7 @@ export interface ConsoleSettings {
emulateV4Engine?: ConsoleSettingsV4EmulationEngine;
alertEngine?: ConsoleSettingsAlertEngine;
licenseExpirationNotification?: ConsoleSettingsLicenseExpirationNotification;
trialInstance?: ConsoleSettingsTrialInstance;
}

export interface ConsoleSettingsEmail {
Expand Down Expand Up @@ -166,3 +167,7 @@ export interface ConsoleSettingsAlertEngine {
export interface ConsoleSettingsLicenseExpirationNotification {
enabled?: boolean;
}

export interface ConsoleSettingsTrialInstance {
enabled?: boolean;
}
Expand Up @@ -268,7 +268,7 @@ <h2 gioTableOfContents>CORS</h2>
</mat-card-content>
</mat-card>

<mat-card class="settings-general__form__card" formGroupName="email">
<mat-card *ngIf="!settings?.trialInstance?.enabled" class="settings-general__form__card" formGroupName="email">
<mat-card-content>
<h2 gioTableOfContents>SMTP</h2>
<gio-form-slide-toggle
Expand Down
Expand Up @@ -152,4 +152,9 @@ public String getDisplayName() {

return displayName;
}

@JsonIgnore
public boolean optedIn() {
return "ACTIVE".equalsIgnoreCase(status) && password != null || "memory".equalsIgnoreCase(source);
}
}
Expand Up @@ -260,17 +260,21 @@ public enum Key {
),
PORTAL_HTTP_CORS_MAX_AGE("http.api.portal.cors.max-age", "1728000", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM))),

EMAIL_ENABLED("email.enabled", "false", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM))),
EMAIL_HOST("email.host", "smtp.my.domain", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM))),
EMAIL_PORT("email.port", "587", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM))),
EMAIL_USERNAME("email.username", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM))),
EMAIL_PASSWORD("email.password", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM))),
EMAIL_PROTOCOL("email.protocol", "smtp", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM))),
EMAIL_SUBJECT("email.subject", "[Gravitee.io] %s", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM))),
EMAIL_FROM("email.from", "noreply@my.domain", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM))),
EMAIL_PROPERTIES_AUTH_ENABLED("email.properties.auth", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM))),
EMAIL_PROPERTIES_STARTTLS_ENABLE("email.properties.starttls.enable", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM))),
EMAIL_PROPERTIES_SSL_TRUST("email.properties.ssl.trust", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM))),
EMAIL_ENABLED("email.enabled", "false", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM)), true),
EMAIL_HOST("email.host", "smtp.my.domain", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM)), true),
EMAIL_PORT("email.port", "587", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM)), true),
EMAIL_USERNAME("email.username", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM)), true),
EMAIL_PASSWORD("email.password", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM)), true),
EMAIL_PROTOCOL("email.protocol", "smtp", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM)), true),
EMAIL_SUBJECT("email.subject", "[Gravitee.io] %s", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM)), true),
EMAIL_FROM("email.from", "noreply@my.domain", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM)), true),
EMAIL_PROPERTIES_AUTH_ENABLED("email.properties.auth", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM)), true),
EMAIL_PROPERTIES_STARTTLS_ENABLE(
"email.properties.starttls.enable",
new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM)),
true
),
EMAIL_PROPERTIES_SSL_TRUST("email.properties.ssl.trust", new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM)), true),

API_LABELS_DICTIONARY("api.labelsDictionary", List.class, new HashSet<>(Arrays.asList(ENVIRONMENT, ORGANIZATION, SYSTEM))),
API_PRIMARY_OWNER_MODE(
Expand Down Expand Up @@ -357,13 +361,15 @@ public enum Key {

ALERT_ENGINE_ENABLED("alerts.alert-engine.enabled", "false", Set.of(SYSTEM)),

INSTALLATION_TYPE("installation.type", "standalone", Set.of(SYSTEM));
INSTALLATION_TYPE("installation.type", "standalone", Set.of(SYSTEM)),
TRIAL_INSTANCE("trialInstance.enabled", "false", Set.of(SYSTEM));

String key;
String defaultValue;
Class<?> type;
boolean isOverridable = true;
Set<KeyScope> scopes;
boolean isHiddenForTrial = false;

Key(String key, Set<KeyScope> scopes) {
this.key = key;
Expand Down Expand Up @@ -396,6 +402,19 @@ public enum Key {
this.scopes = scopes;
}

Key(String key, String defaultValue, Set<KeyScope> scopes, boolean isHiddenForTrial) {
this.key = key;
this.defaultValue = defaultValue;
this.scopes = scopes;
this.isHiddenForTrial = isHiddenForTrial;
}

Key(String key, Set<KeyScope> scopes, boolean isHiddenForTrial) {
this.key = key;
this.scopes = scopes;
this.isHiddenForTrial = isHiddenForTrial;
}

public static Key findByKey(String value) {
for (Key key : Key.values()) {
if (key.key.equals(value)) {
Expand All @@ -421,6 +440,10 @@ public boolean isOverridable() {
return isOverridable;
}

public boolean isHiddenForTrial() {
return isHiddenForTrial;
}

public Set<KeyScope> scopes() {
return scopes;
}
Expand Down
Expand Up @@ -35,6 +35,7 @@ public class ConsoleConfigEntity {
private V4EmulationEngine v4EmulationEngine;
private AlertEngine alertEngine;
private LicenseExpirationNotification licenseExpirationNotification;
private TrialInstance trialInstance;

public ConsoleConfigEntity() {
super();
Expand All @@ -50,5 +51,6 @@ public ConsoleConfigEntity() {
v4EmulationEngine = new V4EmulationEngine();
alertEngine = new AlertEngine();
licenseExpirationNotification = new LicenseExpirationNotification();
trialInstance = new TrialInstance();
}
}
Expand Up @@ -45,6 +45,7 @@ public class ConsoleSettingsEntity extends AbstractCommonSettingsEntity {
private V4EmulationEngine v4EmulationEngine;
private AlertEngine alertEngine;
private LicenseExpirationNotification licenseExpirationNotification;
private TrialInstance trialInstance;

public ConsoleSettingsEntity() {
super();
Expand All @@ -61,6 +62,7 @@ public ConsoleSettingsEntity() {
v4EmulationEngine = new V4EmulationEngine();
alertEngine = new AlertEngine();
licenseExpirationNotification = new LicenseExpirationNotification();
trialInstance = new TrialInstance();
}

//Classes
Expand Down
@@ -0,0 +1,35 @@
/*
* Copyright © 2015 The Gravitee team (http://gravitee.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.gravitee.rest.api.model.settings;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.gravitee.rest.api.model.annotations.ParameterKey;
import io.gravitee.rest.api.model.parameters.Key;
import lombok.Getter;
import lombok.Setter;

/**
* @author Yann TAVERNIER (yann.tavernier at graviteesource.com)
* @author GraviteeSource Team
*/
@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
public class TrialInstance {

@ParameterKey(Key.TRIAL_INSTANCE)
private Boolean enabled;
}
@@ -0,0 +1,41 @@
/*
* Copyright © 2015 The Gravitee team (http://gravitee.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.gravitee.rest.api.service;

import io.gravitee.rest.api.service.common.ExecutionContext;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

public interface EmailRecipientsService {
/**
* Process a list of templated recipients to extract it as a list of unique email addresses
*
* @param templatedRecipientsEmail a list of strings representing templated email. Each string can be itself a literal list of recipients separated by ' ' (whitespace) ',' or ';'. If an element contains '$', then it will be processed against templateData parameter with Freemarker processor
* @param templateData is the dateset used to process emails.
* @return a set of not empty emails.
*/
Set<String> processTemplatedRecipients(Collection<String> templatedRecipientsEmail, Map<String, Object> templateData);

/**
* Checks that each email has an opted-in user attached to it. If it is not the case, then the email is excluded from the result.
* @param executionContext
* @param recipientsEmail is the list of recipients to verify if attached user has opted-in. This method assumes emails are valids.
* @return a set of emails attached to an opted-in user.
*/
Set<String> filterRegisteredUser(ExecutionContext executionContext, Collection<String> recipientsEmail);
}
Expand Up @@ -136,6 +136,7 @@ public ConsoleSettingsEntity getConsoleSettings(ExecutionContext executionContex
enhanceFromConfigFile(consoleConfigEntity);

enhanceFromRepository(executionContext, consoleConfigEntity);
hideForTrialInstance(consoleConfigEntity);

return consoleConfigEntity;
}
Expand Down Expand Up @@ -309,6 +310,12 @@ private void enhanceFromRepository(final ExecutionContext executionContext, fina
}
}

private void hideForTrialInstance(final ConsoleSettingsEntity consoleConfigEntity) {
if (Boolean.TRUE.equals(consoleConfigEntity.getTrialInstance().getEnabled())) {
consoleConfigEntity.setEmail(null);
}
}

@Override
public void save(ExecutionContext executionContext, PortalSettingsEntity portalSettingsEntity) {
Object[] objects = getObjectArray(portalSettingsEntity);
Expand All @@ -318,19 +325,47 @@ public void save(ExecutionContext executionContext, PortalSettingsEntity portalS
@Override
public void save(ExecutionContext executionContext, ConsoleSettingsEntity consoleSettingsEntity) {
Object[] objects = getObjectArray(consoleSettingsEntity);
saveConfigByReference(executionContext, objects, executionContext.getOrganizationId(), ParameterReferenceType.ORGANIZATION);
saveConfigByReference(
executionContext,
objects,
executionContext.getOrganizationId(),
ParameterReferenceType.ORGANIZATION,
isTrialInstance(consoleSettingsEntity)
);
}

private boolean isTrialInstance(ConsoleSettingsEntity consoleSettings) {
return (
consoleSettings.getTrialInstance() != null &&
consoleSettings.getTrialInstance().getEnabled() != null &&
consoleSettings.getTrialInstance().getEnabled()
);
}

private void saveConfigByReference(
ExecutionContext executionContext,
Object[] objects,
String referenceId,
ParameterReferenceType referenceType
) {
saveConfigByReference(executionContext, objects, referenceId, referenceType, false);
}

private void saveConfigByReference(
ExecutionContext executionContext,
Object[] objects,
String referenceId,
ParameterReferenceType referenceType,
boolean isTrialInstance
) {
for (Object o : objects) {
for (Field f : o.getClass().getDeclaredFields()) {
ParameterKey parameterKey = f.getAnnotation(ParameterKey.class);
if (parameterKey != null) {
// do not save parameters that are hidden for trial instances
if (isTrialInstance && parameterKey.value().isHiddenForTrial()) {
continue;
}
boolean accessible = f.isAccessible();
f.setAccessible(true);
try {
Expand Down Expand Up @@ -443,6 +478,7 @@ private Object[] getObjectArray(ConsoleSettingsEntity consoleConfigEntity) {
consoleConfigEntity.getV4EmulationEngine(),
consoleConfigEntity.getAlertEngine(),
consoleConfigEntity.getLicenseExpirationNotification(),
consoleConfigEntity.getTrialInstance(),
};
}
}
@@ -0,0 +1,83 @@
/*
* Copyright © 2015 The Gravitee team (http://gravitee.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.gravitee.rest.api.service.impl;

import static java.util.function.Predicate.not;

import io.gravitee.apim.core.template.TemplateProcessor;
import io.gravitee.apim.core.template.TemplateProcessorException;
import io.gravitee.rest.api.model.UserEntity;
import io.gravitee.rest.api.service.EmailRecipientsService;
import io.gravitee.rest.api.service.UserService;
import io.gravitee.rest.api.service.common.ExecutionContext;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

@Component
@AllArgsConstructor
@Slf4j
public class EmailRecipientsServiceImpl implements EmailRecipientsService {

public static final Pattern SPLIT_PATTERN = Pattern.compile("[,;\\s]");
private final TemplateProcessor templateProcessor;
private final UserService userService;

@Override
public Set<String> processTemplatedRecipients(Collection<String> templatedRecipientsEmail, final Map<String, Object> templateData) {
return templatedRecipientsEmail
.stream()
.flatMap(splittableRecipientsStr ->
Arrays
.stream(SPLIT_PATTERN.split(splittableRecipientsStr))
.filter(not(String::isEmpty))
.map(recipient -> {
if (recipient.contains("$")) {
try {
return Optional.ofNullable(templateProcessor.processInlineTemplate(recipient, templateData));
} catch (TemplateProcessorException e) {
log.error("Error while processing template '{}' skipping this email", recipient, e);
return Optional.<String>empty();
}
}
return Optional.of(recipient);
})
)
.flatMap(Optional::stream)
.filter(not(StringUtils::isEmpty))
.collect(Collectors.toSet());
}

@Override
public Set<String> filterRegisteredUser(ExecutionContext executionContext, Collection<String> recipientsEmail) {
return recipientsEmail
.stream()
.map(email -> userService.findByEmail(executionContext, email))
.flatMap(Optional::stream)
.filter(UserEntity::optedIn)
.map(UserEntity::getEmail)
.collect(Collectors.toSet());
}
}

0 comments on commit 6a6c2d1

Please sign in to comment.