package com.airlock.iam.authentication.application.configuration.kerberos;

import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.airlock.iam.flow.api.application.configuration.selection.FlowConditionConfig;
import com.airlock.iam.flow.api.application.configuration.step.AbstractFlowStepConfig;
import com.airlock.iam.flow.api.application.configuration.step.NonInteractiveStepConfig;
import com.airlock.iam.flow.api.application.configuration.step.StepIdConfig;
import com.airlock.iam.flow.api.application.configuration.tag.TagConfig;
import com.airlock.iam.flow.api.application.configuration.valueprovider.ValueMapProviderConfig;
import com.airlock.iam.flow.shared.domain.model.step.access.AccessFlowStep;
import com.airlock.iam.flow.types.api.application.configuration.step.StepConfigForAuthenticationFlows;
import com.airlock.iam.plugin.api.annotation.config.Groups;
import com.airlock.iam.plugin.api.annotation.config.NextActions;
import com.airlock.iam.plugin.api.annotation.config.Plugin;
import com.airlock.iam.plugin.api.annotation.lifecycle.AfterConfigActivation;
import com.airlock.iam.plugin.api.annotation.property.BooleanProperty;
import com.airlock.iam.plugin.api.annotation.property.FileProperty;
import com.airlock.iam.plugin.api.annotation.property.GroupProperty;
import com.airlock.iam.plugin.api.annotation.property.StringProperty;
import com.airlock.iam.plugin.api.exception.InitializeException;
import com.google.common.base.Preconditions;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.annotation.Nullable;
import java.io.File;
import java.security.Principal;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import org.apache.commons.lang3.StringUtils;

@Plugin(
   displayName = "Kerberos Authentication Step",
   description = "<p>Performs a Kerberos SPNEGO authentication handshake.</p>\n<p>If the client does not support Kerberos, the step fails with error code <tt>KERBEROS_AUTHENTICATION_NOT_POSSIBLE</tt>.\nUse On Failure Gotos on this error code (and possibly also on <tt>KERBEROS_TICKET_VERIFICATION_FAILED</tt>) to switch to an\nalternative authentication method.</p>"
)
@Groups(openGroups = "Basic Settings", groupSequence = {"Basic Settings", "Advanced Settings", "Flow Control"})
@NextActions(AccessFlowStep.AccessStepActions.class)
public final class KerberosAuthStepConfig
   extends AbstractFlowStepConfig
   implements KerberosAuthStepConfigBase,
   StepConfigForAuthenticationFlows,
   NonInteractiveStepConfig {
   private static final ELogger LOG = ELoggerFactory.getLogger(KerberosAuthStepConfig.class);
   private static final String SUN_SECURITY_KRB5_PRINCIPAL = "sun.security.krb5.principal";
   @FileProperty(
      description = "The path of the Kerberos keytab file to be used for Kerberos SPNEGO identity assertion.",
      optional = false,
      assureExistence = true,
      isFile = true,
      assureReadable = true,
      exampleValues = "/etc/medusa.keytab"
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private File keytabFile;
   @StringProperty(
      description = "The Kerberos Service Principal Name (SPN) associated with Airlock IAM. It is usually in the form\n<tt>HTTP/<i>&lt;FQDN&gt;</i></tt> or <tt>*</tt> to allow all SPNs present in the keytab file.<br/>\nOptionally, the realm can be specified if there's no default realm in the Kerberos configuration file or if\nthe SPN is associated with a different realm/domain.",
      optional = false,
      exampleValues = {"HTTP/login.ergon.ch", "HTTP/login.ergon.ch@REALM", "*"}
   )
   @GroupProperty(groupIdentifier = "Basic Settings")
   private String servicePrincipal;
   @BooleanProperty(description = "Strip the domain name part from the resulting username.", optional = true, defaultValue = "true")
   @GroupProperty(groupIdentifier = "Basic Settings")
   private boolean stripDomainFromUsername;
   @StringProperty(
      description = "The identifier of the authentication method for this step. Since the authentication method is also the identifier for failed login attempts for this step, distinct identifiers must be chosen if multiple instances of the same step type are used to check different credentials (e.g., two password step instances check two different passwords).<p>Example: a system tracks two passwords per user. Password 1 is used in flow A, password 2 in flow B. These two steps must have distinct 'Authentication Method Ids', e.g. PASSWORD1 and PASSWORD2.</p>If only one identifier (e.g. \"PASSWORD\") is used, this may allow brute force attacks on the password as follows. Assuming an attacker knows the user's password 1, they get an unlimited number of attempts on flow B, as the 'PASSWORD' counter can repeatedly be reset to 0 by performing a successful login on flow A.<p>To prevent such attacks, use two different counters by setting the authentication methods, for example, to PASSWORD1 and PASSWORD2, respectively.</p>",
      displayName = "Authentication Method ID",
      defaultValue = "KERBEROS",
      maxLength = 23,
      optional = true
   )
   @GroupProperty(groupIdentifier = "Advanced Settings")
   private String authenticationMethodId;
   private Subject serviceSubject;

   private KerberosAuthStepConfig(
      @Nullable FlowConditionConfig skipCondition,
      @Nullable FlowConditionConfig preCondition,
      boolean requiresActivation,
      List<TagConfig> tagsOnSuccess,
      @Nullable StepIdConfig stepId,
      Map<String, StepIdConfig> onFailureGotos,
      List<ValueMapProviderConfig> customResponseAttributes,
      List<ValueMapProviderConfig> customFailureResponseAttributes,
      File keytabFile,
      String servicePrincipal,
      boolean stripDomainFromUsername,
      String authenticationMethodId
   ) {
      super(skipCondition, preCondition, requiresActivation, tagsOnSuccess, stepId, onFailureGotos, customResponseAttributes, customFailureResponseAttributes);
      this.keytabFile = keytabFile;
      this.servicePrincipal = servicePrincipal;
      this.stripDomainFromUsername = stripDomainFromUsername;
      this.authenticationMethodId = authenticationMethodId;
   }

   public Subject getServiceSubject() {
      return (Subject)Preconditions.checkNotNull(this.serviceSubject, "@AfterConfigActivation not called.");
   }

   @AfterConfigActivation
   void afterConfigActivation() throws InitializeException {
      this.checkKerberosSystemProperties();
      this.createServiceSubject();
   }

   private void checkKerberosSystemProperties() {
      if (System.getProperty("sun.security.krb5.principal") != null) {
         LOG.error(
            LogTypes.CONFIGURATION_ERROR,
            "Kerberos related Java System Property \"sun.security.krb5.principal\" set to \""
               + System.getProperty("sun.security.krb5.principal")
               + "\". Will interfere with Kerberos SPNEGO authentication!"
         );
      }
   }

   private void createServiceSubject() throws InitializeException {
      try {
         Configuration loginConfig = new KerberosLoginConfig(this.keytabFile.getPath(), this.servicePrincipal, false);
         Set<Principal> princ = Set.of(new KerberosPrincipal(this.servicePrincipal));
         Subject sub = new Subject(false, princ, new HashSet(), new HashSet());
         LoginContext lc = new LoginContext("", sub, null, loginConfig);
         lc.login();
         if (lc.getSubject() == null) {
            LOG.error(com.airlock.iam.core.misc.tanserver.LogTypes.CONFIGURATION_ERROR, "Could not login Service Subject");
            throw new InitializeException("Could not login Service Subject");
         }

         this.serviceSubject = lc.getSubject();
         LOG.debug("Initialized Kerberos SPNEGO config for: " + StringUtils.normalizeSpace(lc.getSubject().toString()));
      } catch (RuntimeException e) {
         LOG.error(
            com.airlock.iam.core.misc.tanserver.LogTypes.CONFIGURATION_ERROR,
            "Invalid service principal name or invalid realm: " + this.servicePrincipal + ": " + e.getMessage()
         );
         throw new InitializeException("Invalid service principal name or invalid realm: " + this.servicePrincipal + ": " + e.getMessage(), e);
      } catch (LoginException e) {
         LOG.error(com.airlock.iam.core.misc.tanserver.LogTypes.CONFIGURATION_ERROR, "Could not login Service Subject:" + e.getMessage(), e);
         throw new InitializeException("Could not login the Service Subject:" + e.getMessage(), e);
      }
   }

   public File getKeytabFile() {
      return this.keytabFile;
   }

   public String getServicePrincipal() {
      return this.servicePrincipal;
   }

   public boolean isStripDomainFromUsername() {
      return this.stripDomainFromUsername;
   }

   public String getAuthenticationMethodId() {
      return this.authenticationMethodId;
   }

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

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

   @SuppressFBWarnings(justification = "generated code")
   public static class KerberosAuthStepConfigBuilder {
      @SuppressFBWarnings(justification = "generated code")
      private FlowConditionConfig skipCondition;
      @SuppressFBWarnings(justification = "generated code")
      private FlowConditionConfig preCondition;
      @SuppressFBWarnings(justification = "generated code")
      private boolean requiresActivation;
      @SuppressFBWarnings(justification = "generated code")
      private List<TagConfig> tagsOnSuccess;
      @SuppressFBWarnings(justification = "generated code")
      private StepIdConfig stepId;
      @SuppressFBWarnings(justification = "generated code")
      private Map<String, StepIdConfig> onFailureGotos;
      @SuppressFBWarnings(justification = "generated code")
      private List<ValueMapProviderConfig> customResponseAttributes;
      @SuppressFBWarnings(justification = "generated code")
      private List<ValueMapProviderConfig> customFailureResponseAttributes;
      @SuppressFBWarnings(justification = "generated code")
      private File keytabFile;
      @SuppressFBWarnings(justification = "generated code")
      private String servicePrincipal;
      @SuppressFBWarnings(justification = "generated code")
      private boolean stripDomainFromUsername;
      @SuppressFBWarnings(justification = "generated code")
      private String authenticationMethodId;

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

      @SuppressFBWarnings(justification = "generated code")
      public KerberosAuthStepConfig.KerberosAuthStepConfigBuilder skipCondition(@Nullable FlowConditionConfig skipCondition) {
         this.skipCondition = skipCondition;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public KerberosAuthStepConfig.KerberosAuthStepConfigBuilder preCondition(@Nullable FlowConditionConfig preCondition) {
         this.preCondition = preCondition;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public KerberosAuthStepConfig.KerberosAuthStepConfigBuilder requiresActivation(boolean requiresActivation) {
         this.requiresActivation = requiresActivation;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public KerberosAuthStepConfig.KerberosAuthStepConfigBuilder tagsOnSuccess(List<TagConfig> tagsOnSuccess) {
         this.tagsOnSuccess = tagsOnSuccess;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public KerberosAuthStepConfig.KerberosAuthStepConfigBuilder stepId(@Nullable StepIdConfig stepId) {
         this.stepId = stepId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public KerberosAuthStepConfig.KerberosAuthStepConfigBuilder onFailureGotos(Map<String, StepIdConfig> onFailureGotos) {
         this.onFailureGotos = onFailureGotos;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public KerberosAuthStepConfig.KerberosAuthStepConfigBuilder customResponseAttributes(List<ValueMapProviderConfig> customResponseAttributes) {
         this.customResponseAttributes = customResponseAttributes;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public KerberosAuthStepConfig.KerberosAuthStepConfigBuilder customFailureResponseAttributes(List<ValueMapProviderConfig> customFailureResponseAttributes) {
         this.customFailureResponseAttributes = customFailureResponseAttributes;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public KerberosAuthStepConfig.KerberosAuthStepConfigBuilder keytabFile(File keytabFile) {
         this.keytabFile = keytabFile;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public KerberosAuthStepConfig.KerberosAuthStepConfigBuilder servicePrincipal(String servicePrincipal) {
         this.servicePrincipal = servicePrincipal;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public KerberosAuthStepConfig.KerberosAuthStepConfigBuilder stripDomainFromUsername(boolean stripDomainFromUsername) {
         this.stripDomainFromUsername = stripDomainFromUsername;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public KerberosAuthStepConfig.KerberosAuthStepConfigBuilder authenticationMethodId(String authenticationMethodId) {
         this.authenticationMethodId = authenticationMethodId;
         return this;
      }

      @SuppressFBWarnings(justification = "generated code")
      public KerberosAuthStepConfig build() {
         return new KerberosAuthStepConfig(
            this.skipCondition,
            this.preCondition,
            this.requiresActivation,
            this.tagsOnSuccess,
            this.stepId,
            this.onFailureGotos,
            this.customResponseAttributes,
            this.customFailureResponseAttributes,
            this.keytabFile,
            this.servicePrincipal,
            this.stripDomainFromUsername,
            this.authenticationMethodId
         );
      }

      @SuppressFBWarnings(justification = "generated code")
      @Override
      public String toString() {
         return "KerberosAuthStepConfig.KerberosAuthStepConfigBuilder(skipCondition="
            + this.skipCondition
            + ", preCondition="
            + this.preCondition
            + ", requiresActivation="
            + this.requiresActivation
            + ", tagsOnSuccess="
            + this.tagsOnSuccess
            + ", stepId="
            + this.stepId
            + ", onFailureGotos="
            + this.onFailureGotos
            + ", customResponseAttributes="
            + this.customResponseAttributes
            + ", customFailureResponseAttributes="
            + this.customFailureResponseAttributes
            + ", keytabFile="
            + this.keytabFile
            + ", servicePrincipal="
            + this.servicePrincipal
            + ", stripDomainFromUsername="
            + this.stripDomainFromUsername
            + ", authenticationMethodId="
            + this.authenticationMethodId
            + ")";
      }
   }
}
