package com.airlock.iam.login.application.configuration.location.propagate;

import com.airlock.iam.core.misc.plugin.config.validation.UriPropertyValidator;
import com.airlock.iam.login.api.application.configuration.location.propagate.IdentityPropagatorURITransformerConfig;
import com.airlock.iam.plugin.api.annotation.config.ConfigurationValidator;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.property.PluginListProperty;
import com.airlock.iam.plugin.api.annotation.property.RegexListProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
import com.airlock.iam.plugin.api.config.ConfigurationContext;
import com.airlock.iam.plugin.api.config.ConfigurationEnvironment;
import com.airlock.iam.plugin.api.config.Environment;
import com.airlock.iam.plugin.api.config.PluginConfiguration;
import com.airlock.iam.plugin.api.exception.ConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConstraintViolationException;
import com.airlock.iam.plugin.api.valuereader.RegexListPropertyValueReader;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

@Plugin(
   description = "Uses the original forward location (extracted from the URI parameter 'Location') to resolve the target URI for identity propagation. <p> If the original forward location matches any of the allowed URI patterns, the forward location is passed to the chain of URI transformers, where it gets transformed to the target URI to be used for identity propagation. If none of the allowed URI pattern matches the forward location, the default value is instead passed to the transformation chain. If any of the URI transformers vetoes the transformation, the default value is provided as target URI. <p>This is typically used to allow or block certain deep links into target applications and to add information to the URI like a language parameter.",
   displayName = "Target URI Resolver"
)
public class TargetURIResolverConfig {
   public static final String ALLOWED_FORWARD_LOCATION_PATTERN_SECURITY_WARNING = "<b>Security warning:</b> The provided forward location may contain arbitrary user input! It is therefore highly recommended that the configured patterns be as restrictive as possible. Overly lax patterns like '.*' expose Airlock IAM and the target application to severe attacks, e.g.: open redirect, server-side request forgery and injection attacks (SQL, XSS, etc ...). Such patterns are therefore not allowed.";
   @StringProperty(
      description = "The default URI to be used as input for transformation if the original forward location is not allowed. This URI is also used if an URI transformer produces a veto.",
      exampleValues = {"/", "https://domain.com/public"},
      displayName = "Default Value",
      optional = false
   )
   private String defaultValue;
   public static final String DEFAULT_VALUE_FIELD = "defaultValue";
   public static final String DEFAULT_VALUE_DISPLAY_VALUE = "Default Value";
   @RegexListProperty(
      description = "A list of regular expressions defining the allowed forward locations.<br/>If the authentication flow was started using such a location, its value is matched against the specified list of allowed location parameter patterns. If at least one matches, the location parameter is accepted. If not, it is not accepted and the \"Default Value\" is used instead.<ul><li>If left empty, any provided location is ignored and the default value is always used.</li><li>To only allow context relative locations (starting with a slash), the pattern \"/(?!/).*\" should be used. The pattern \"/.*\" is <b>not</b> sufficient as it allows URLs starting with \"//\" which can be used to specify an absolute URL including a domain. See also <a href=\"https://techzone.ergon.ch/CVE-2013-2764\">CVE-2013-2764</a>.</li><li>Note that forward locations provided by Airlock Gateway (WAF) always contain the full URL including protocol and hostname. It is therefore usually necessary to add a pattern for the application specific URLs, e.g. \"https?://example\\.com/my-path/.*\".</li><li>Note that URLs with any 'User Information' part in front of the host name (for example \"https://user@domain.com/\") are <b>never</b> accepted.</li><li><b>Security warning:</b> The provided forward location may contain arbitrary user input! It is therefore highly recommended that the configured patterns be as restrictive as possible. Overly lax patterns like '.*' expose Airlock IAM and the target application to severe attacks, e.g.: open redirect, server-side request forgery and injection attacks (SQL, XSS, etc ...). Such patterns are therefore not allowed.</li><li><b>Security warning:</b> Unescaped dots in URLs match <b>any</b> single character in RegExes. If unescaped dots are used by mistake, this can be exploited for attacks! E.g.: If pattern \"https://subdomain.example.com/.*\" is configured, attackers can register \"subdomain-example.com\" which will be allowed by this pattern.</li></ul>",
      displayName = "Allowed URIs",
      optional = true,
      exampleElements = {"/appl1/.*\\.html", "https?://domain\\.com/.*", "/(?!/).*"}
   )
   private List<Pattern> allowedURIs;
   public static final String ALLOWED_URIS_VALUE_FIELD = "allowedURIs";
   private static final List<String> OPEN_REDIRECTS_CANARIES = List.of(
      "http://verifyopenrelay.org",
      "https://verifyopenrelay.org",
      "//verifyopenrelay.org",
      "http://verifyopenrelay.org/x",
      "https://verifyopenrelay.org/x",
      "//verifyopenrelay.org/x"
   );
   @PluginListProperty(
      description = "The chain of URI transformers that transform the forward location to the provided target URI.",
      displayName = "URI Transformers",
      optional = true,
      minElements = 1
   )
   private List<IdentityPropagatorURITransformerConfig> uriTransformers;

   public List<IdentityPropagatorURITransformerConfig> getURITransformers() {
      return this.uriTransformers;
   }

   @ConfigurationValidator
   public static void checkDefaultValue(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      UriPropertyValidator.uriPropertyValidator("defaultValue", "Default Value", config, env, context).validateIsUri();
   }

   @ConfigurationValidator
   public static void checkForOpenRedirects(
      PluginConfiguration config, ConfigurationEnvironment env, ConfigurationContext context, Environment validationEnvironment
   ) throws ConfigurationException {
      List<String> allowedURIs = RegexListPropertyValueReader.effectiveRegexListConfigValue(config, "allowedURIs", env, context);
      if (isOpenRedirectAllowed(allowedURIs)) {
         throw new PropertyConstraintViolationException(
            "allowedURIs",
            config,
            env,
            context,
            "The configured list of allowed URIs allows access to any domain (\"open redirect\"), thus potentially allowing access to unsafe sites."
         );
      }
   }

   private static boolean isOpenRedirectAllowed(List<String> allowedURIs) {
      return allowedURIs.stream().anyMatch(allowedURI -> OPEN_REDIRECTS_CANARIES.stream().anyMatch(canary -> canary.matches(allowedURI)));
   }

   public String getDefaultValue() {
      return this.defaultValue;
   }

   public List<Pattern> getAllowedURIs() {
      return this.allowedURIs;
   }

   @SuppressFBWarnings(justification = "generated code")
   public static TargetURIResolverConfig.TargetURIResolverConfigBuilder builder() {
      return new TargetURIResolverConfig.TargetURIResolverConfigBuilder();
   }

   @SuppressFBWarnings(justification = "generated code")
   public TargetURIResolverConfig(String defaultValue, List<Pattern> allowedURIs, List<IdentityPropagatorURITransformerConfig> uriTransformers) {
      this.defaultValue = defaultValue;
      this.allowedURIs = allowedURIs;
      this.uriTransformers = uriTransformers;
   }

   @SuppressFBWarnings(justification = "generated code")
   public TargetURIResolverConfig() {
   }

   @SuppressFBWarnings(justification = "generated code")
   public static class TargetURIResolverConfigBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private String defaultValue;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<Pattern> allowedURIs;
      @SuppressFBWarnings(justification = "generated code")
      private ArrayList<IdentityPropagatorURITransformerConfig> uriTransformers;

      @SuppressFBWarnings(justification = "generated code")
      TargetURIResolverConfigBuilder() {
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetURIResolverConfig.TargetURIResolverConfigBuilder defaultValue(String defaultValue) {
         this.defaultValue = defaultValue;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetURIResolverConfig.TargetURIResolverConfigBuilder allowedURI(Pattern allowedURI) {
         if (this.allowedURIs == null) {
            this.allowedURIs = new ArrayList<>();
         }

         this.allowedURIs.add(allowedURI);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetURIResolverConfig.TargetURIResolverConfigBuilder allowedURIs(Collection<? extends Pattern> allowedURIs) {
         if (allowedURIs == null) {
            throw new NullPointerException("allowedURIs cannot be null");
         }

         if (this.allowedURIs == null) {
            this.allowedURIs = new ArrayList<>();
         }

         this.allowedURIs.addAll(allowedURIs);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetURIResolverConfig.TargetURIResolverConfigBuilder clearAllowedURIs() {
         if (this.allowedURIs != null) {
            this.allowedURIs.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetURIResolverConfig.TargetURIResolverConfigBuilder uriTransformer(IdentityPropagatorURITransformerConfig uriTransformer) {
         if (this.uriTransformers == null) {
            this.uriTransformers = new ArrayList<>();
         }

         this.uriTransformers.add(uriTransformer);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetURIResolverConfig.TargetURIResolverConfigBuilder uriTransformers(
         Collection<? extends IdentityPropagatorURITransformerConfig> uriTransformers
      ) {
         if (uriTransformers == null) {
            throw new NullPointerException("uriTransformers cannot be null");
         }

         if (this.uriTransformers == null) {
            this.uriTransformers = new ArrayList<>();
         }

         this.uriTransformers.addAll(uriTransformers);
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetURIResolverConfig.TargetURIResolverConfigBuilder clearUriTransformers() {
         if (this.uriTransformers != null) {
            this.uriTransformers.clear();
         }

         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public TargetURIResolverConfig build() {
         List allowedURIs = switch (this.allowedURIs == null ? 0 : this.allowedURIs.size()) {
            case 0 -> Collections.emptyList();
            case 1 -> Collections.singletonList(this.allowedURIs.get(0));
            default -> Collections.unmodifiableList(new ArrayList<>(this.allowedURIs));
         };

         return new TargetURIResolverConfig(this.defaultValue, allowedURIs, switch (this.uriTransformers == null ? 0 : this.uriTransformers.size()) {
            case 0 -> Collections.emptyList();
            case 1 -> Collections.singletonList(this.uriTransformers.get(0));
            default -> Collections.unmodifiableList(new ArrayList<>(this.uriTransformers));
         });
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "TargetURIResolverConfig.TargetURIResolverConfigBuilder(defaultValue="
            + this.defaultValue
            + ", allowedURIs="
            + this.allowedURIs
            + ", uriTransformers="
            + this.uriTransformers
            + ")";
      }
   }
}
