Skip to content

Commit

Permalink
Add reCAPTCHA and email domain validation
Browse files Browse the repository at this point in the history
  • Loading branch information
arteymix committed Dec 1, 2023
1 parent 43ac5d1 commit bb8cda4
Show file tree
Hide file tree
Showing 21 changed files with 754 additions and 19 deletions.
52 changes: 44 additions & 8 deletions docs/customization.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
# Customize your instance

## Allowed email domains (new in 1.5.8)

You may restrict the email domains that can be used for creating new accounts by specifying a file containing one line
per domain. Matches are performed in a case-insensitive manner.

```properties
rdp.settings.allowed-email-domains-file=file:swot.txt
rdp.settings.allowed-email-domains-refresh-delay=3600
```

This feature is disabled by default.

Note that [internationalized domains](https://en.wikipedia.org/wiki/Internationalized_domain_name) are not allowed and
will be ignored from the file.

The default refresh delay is set to one hour. To disable it, you can set `rdp.settings.allowed-email-domains-refresh-delay`
to empty.

There's a few projects out there that curate institutional email addresses which should be generally suitable

Refer to [JetBrains/swot](https://github.com/JetBrains/swot) for a list of institu

## reCAPTCHA (new in 1.5.8)

RDP supports [reCAPTCHA v2](https://www.google.com/recaptcha/about/) to mitigate the registration of spam accounts by
bots. To enable it, add the reCAPTCHA secret to your configuration.

```properties
rdp.settings.recaptcha-secret=mysecret
```

This feature is disabled by default.

## Cached data

Most of the data used by the application is retrieved remotely at startup and subsequently updated on a monthly basis.

To prevent data from being loaded on startup and/or recurrently, set the following parameter in
Expand All @@ -12,6 +47,8 @@ rdp.settings.cache.enabled=false
You should deploy your RDP instance at least once to have initial data before setting this property and whenever you
update the software.

The following sections will cover in details individual data sources that can be imported in your registry.

## Gene information and GO terms

By default, RDP will retrieve the latest gene information from NCBI, and GO terms
Expand Down Expand Up @@ -271,19 +308,20 @@ The page lists some basic stats at the very top and provides few action buttons:

![Actions available for simple categories.](images/simple-category-actions.png)

- "Deactivate" (or "Deactivate All Terms" in the case of an ontology category): this will remove the category from the Profile and Search pages. This action is reversible, as the category can be easily re-activated. This action is recommended in cases where a category cannot be deleted because it has already been used by some users.
- "Deactivate" (or "Deactivate All Terms" in the case of an ontology category): this will remove the category from the
Profile and Search pages. This action is reversible, as the category can be easily re-activated. This action is
recommended in cases where a category cannot be deleted because it has already been used by some users.

- Update from "source": Update the ontology category using the original URL (if available)

- Download as OBO: Download the category as an OBO file



The number of used terms indicate how many terms in the ontology have been associated with associated with users.

In the Edit window on the Manage Profile Category page, you can add a definition/description of the category, which
is used in a tooltip on the Profile Page. You can also specify if this category will be used as a filter on the Gene
Search page. While all active categories will be available on the Researcher Search page, only categories that have "Available for gene search?" checked will be displayed on the Gene Search page.
Search page. While all active categories will be available on the Researcher Search page, only categories that have "
Available for gene search?" checked will be displayed on the Gene Search page.

![Interface for editing the properties of an ontology.](images/edit-an-ontology.png)

Expand Down Expand Up @@ -348,8 +386,6 @@ values. A warning will be displayed in the admin section if this is the case.
Read more about configuring messages in [Customizing the application messages](#customizing-the-applications-messages)
section of this page.



### Resolving external URLs

By default, ontologies and terms are resolved from [OLS](https://www.ebi.ac.uk/ols/index). Reactome pathways get a
Expand Down Expand Up @@ -402,7 +438,6 @@ settings will retrieve all the necessary files relative to the working directory
#this setting relates only to gene info files. Files for all taxons will be stord under gene/
rdp.settings.cache.load-from-disk=true
rdp.settings.cache.gene-files-location=file:genes/

#file for GO ontology
rdp.settings.cache.term-file=file:go.obo
#file for gene GO annotation
Expand Down Expand Up @@ -537,7 +572,8 @@ rdp.faq.questions.<q_key>=A relevant question.
rdp.faq.answers.<q_key>=A plausible answer.
```

The provided default file can be found in [faq.properties](https://github.com/PavlidisLab/rdp/tree/{{ config.extra.git_ref }}/src/main/resources/faq.properties).
The provided default file can be found in [faq.properties](https://github.com/PavlidisLab/rdp/tree/{{
config.extra.git_ref }}/src/main/resources/faq.properties).

### Ordering FAQ entries

Expand Down
47 changes: 47 additions & 0 deletions src/main/java/ubc/pavlab/rdp/ValidationConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package ubc.pavlab.rdp;

import lombok.extern.apachecommons.CommonsLog;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.web.client.RestTemplate;
import ubc.pavlab.rdp.validation.AllowedDomainStrategy;
import ubc.pavlab.rdp.validation.EmailValidator;
import ubc.pavlab.rdp.validation.RecaptchaValidator;
import ubc.pavlab.rdp.validation.ResourceBasedAllowedDomainStrategy;

import java.io.IOException;
import java.time.Duration;
import java.time.temporal.ChronoUnit;

/**
* This configuration provides a few {@link org.springframework.validation.Validator} beans.
*/
@CommonsLog
@Configuration
public class ValidationConfig {

@Bean
public EmailValidator emailValidator(
@Value("${rdp.settings.allowed-email-domains-file}") Resource allowedEmailDomainsFile,
@Value("${rdp.settings.allowed-email-domains-refresh-delay}") @DurationUnit(ChronoUnit.SECONDS) Duration refreshDelay,
@Value("${rdp.settings.allow-internationalized-domain-names}") boolean allowIdn ) throws IOException {
AllowedDomainStrategy strategy;
if ( allowedEmailDomainsFile == null ) {
strategy = ( domain ) -> true;
log.info( "No allowed email domains file specified, all domains will be allowed for newly registered users." );
} else {
log.info( "Reading allowed email domains from " + allowedEmailDomainsFile + "..." );
strategy = new ResourceBasedAllowedDomainStrategy( allowedEmailDomainsFile, refreshDelay );
( (ResourceBasedAllowedDomainStrategy) strategy ).refresh();
}
return new EmailValidator( strategy, allowIdn );
}

@Bean
public RecaptchaValidator recaptchaValidator( @Value("${rdp.settings.recaptcha.secret}") String secret ) {
return new RecaptchaValidator( new RestTemplate(), secret );
}
}
64 changes: 63 additions & 1 deletion src/main/java/ubc/pavlab/rdp/controllers/LoginController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import lombok.extern.apachecommons.CommonsLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -24,6 +25,8 @@
import ubc.pavlab.rdp.services.PrivacyService;
import ubc.pavlab.rdp.services.UserService;
import ubc.pavlab.rdp.settings.ApplicationSettings;
import ubc.pavlab.rdp.validation.EmailValidator;
import ubc.pavlab.rdp.validation.RecaptchaValidator;

import javax.servlet.http.HttpServletRequest;
import java.util.Locale;
Expand All @@ -44,9 +47,49 @@ public class LoginController {
@Autowired
private ApplicationSettings applicationSettings;

@Autowired
private EmailValidator emailValidator;

@Autowired
private RecaptchaValidator recaptchaValidator;

@Autowired
private MessageSource messageSource;

/**
* Wraps a {@link EmailValidator} so that it can be applied to the {@code user.email} nested path.
*/
private class UserEmailValidator implements Validator {

@Override
public boolean supports( Class<?> clazz ) {
return User.class.isAssignableFrom( clazz );
}

@Override
public void validate( Object target, Errors errors ) {
User user = (User) target;
if ( user.getEmail() != null ) {
try {
errors.pushNestedPath( "email" );
ValidationUtils.invokeValidator( emailValidator, user.getEmail(), errors );
} finally {
errors.popNestedPath();
}
}
}
}

@InitBinder("user")
public void configureUserDataBinder( WebDataBinder dataBinder ) {
dataBinder.setAllowedFields( "email", "password", "profile.name", "profile.lastName" );
dataBinder.addValidators( new UserEmailValidator() );
}

@InitBinder("recaptcha")
public void configureRecaptchaDataBinder( WebDataBinder dataBinder ) {
dataBinder.setAllowedFields( "secret" );
dataBinder.addValidators( recaptchaValidator );
}

@GetMapping("/login")
Expand Down Expand Up @@ -87,6 +130,9 @@ public ModelAndView createNewUser( @Validated(User.ValidationUserAccount.class)

// initialize a basic user profile
Profile userProfile = user.getProfile();
if ( userProfile == null ) {
userProfile = new Profile();
}
userProfile.setPrivacyLevel( privacyService.getDefaultPrivacyLevel() );
userProfile.setShared( applicationSettings.getPrivacy().isDefaultSharing() );
userProfile.setHideGenelist( false );
Expand All @@ -105,6 +151,22 @@ public ModelAndView createNewUser( @Validated(User.ValidationUserAccount.class)

if ( bindingResult.hasErrors() ) {
modelAndView.setStatus( HttpStatus.BAD_REQUEST );
// indicate to the mode
boolean isDomainNotAllowed = bindingResult.getFieldErrors( "email" ).stream()
.map( FieldError::getCode )
.anyMatch( "EmailValidator.domainNotAllowed"::equals );
modelAndView.addObject( "domainNotAllowed", isDomainNotAllowed );
if ( isDomainNotAllowed ) {
// this code is not set if the email is not minimally valid, so we can safely parse it
String domain = user.getEmail().split( "@", 2 )[1];
modelAndView.addObject( "domainNotAllowedFrom", user.getEmail() );
modelAndView.addObject( "domainNotAllowedSubject",
messageSource.getMessage( "LoginController.domainNotAllowedSubject",
new String[]{ domain }, locale ) );
modelAndView.addObject( "domainNotAllowedBody",
messageSource.getMessage( "LoginController.domainNotAllowedBody",
new String[]{ user.getEmail(), domain, user.getProfile().getFullName() }, locale ) );
}
} else {
user = userService.create( user );
userService.createVerificationTokenForUser( user, locale );
Expand Down
6 changes: 1 addition & 5 deletions src/main/java/ubc/pavlab/rdp/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.SchemaProperty;
import lombok.*;
import lombok.extern.apachecommons.CommonsLog;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.NaturalId;
import org.hibernate.validator.constraints.Email;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.util.StringUtils;
import ubc.pavlab.rdp.model.enums.PrivacyLevelType;
import ubc.pavlab.rdp.model.enums.TierType;
import ubc.pavlab.rdp.model.ontology.Ontology;
Expand Down Expand Up @@ -89,7 +85,6 @@ public static Comparator<User> getComparator() {
@NaturalId
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@Column(name = "email", unique = true, nullable = false)
@Email(message = "Your email address is not valid.", groups = { ValidationUserAccount.class })
@NotNull(message = "Please provide an email address.", groups = { ValidationUserAccount.class, ValidationServiceAccount.class })
@Size(min = 1, message = "Please provide an email address.", groups = { ValidationUserAccount.class, ValidationServiceAccount.class })
private String email;
Expand Down Expand Up @@ -144,6 +139,7 @@ public static Comparator<User> getComparator() {
private final Set<PasswordResetToken> passwordResetTokens = new HashSet<>();

@Valid
@NotNull
@Embedded
@JsonUnwrapped
private Profile profile;
Expand Down
18 changes: 16 additions & 2 deletions src/main/java/ubc/pavlab/rdp/settings/ApplicationSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,17 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.validation.annotation.Validated;
import ubc.pavlab.rdp.ontology.resolvers.OntologyResolver;
import ubc.pavlab.rdp.model.GeneInfo;
import ubc.pavlab.rdp.model.enums.PrivacyLevelType;
import ubc.pavlab.rdp.model.enums.ResearcherCategory;
import ubc.pavlab.rdp.model.enums.ResearcherPosition;
import ubc.pavlab.rdp.model.enums.TierType;
import ubc.pavlab.rdp.model.ontology.Ontology;
import ubc.pavlab.rdp.ontology.resolvers.OntologyResolver;
import ubc.pavlab.rdp.services.GeneInfoService;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import java.net.URI;
import java.time.Duration;
Expand Down Expand Up @@ -273,4 +272,19 @@ public static class OntologySettings {
* Enabled tier types.
*/
public EnumSet<TierType> enabledTiers;
/**
* File containing allowed email domains for registering users.
* <p>
* May be null, in which case any email address will be allowed.
*/
private Resource allowedEmailDomainsFile;
/**
* Refresh delay to reload the allowed email domains file, in seconds.
*/
@DurationUnit(value = ChronoUnit.SECONDS)
private Duration allowedEmailDomainsRefreshDelay;
/**
* Allow internationalized domain names.
*/
private boolean allowInternationalizedDomainNames;
}
11 changes: 11 additions & 0 deletions src/main/java/ubc/pavlab/rdp/settings/SiteSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,16 @@ public URI getHostUrl() {
@NotEmpty(message = "The admin email must be specified.")
private String adminEmail;

/**
* GA4 tracker.
*/
private String gaTracker;
/**
* Public reCAPTCHA key.
*/
private String recaptchaToken;
/**
* Secret reCAPTCHA key.
*/
private String recaptchaSecret;
}
12 changes: 12 additions & 0 deletions src/main/java/ubc/pavlab/rdp/validation/AllowedDomainStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ubc.pavlab.rdp.validation;

/**
* Defines a strategy to determine if a domain is allowed.
*
* @author poirigui
*/
@FunctionalInterface
public interface AllowedDomainStrategy {

boolean allows( String domain );
}
Loading

0 comments on commit bb8cda4

Please sign in to comment.