IdCommandContext.java

/*
 * Copyright © 2023 Mark Raynsford <code@io7m.com> https://www.io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */


package com.io7m.idstore.server.controller.command_exec;

import com.io7m.idstore.database.api.IdDatabaseException;
import com.io7m.idstore.database.api.IdDatabaseTransactionType;
import com.io7m.idstore.error_codes.IdErrorCode;
import com.io7m.idstore.model.IdEmail;
import com.io7m.idstore.model.IdPasswordException;
import com.io7m.idstore.model.IdValidityException;
import com.io7m.idstore.protocol.api.IdProtocolException;
import com.io7m.idstore.protocol.api.IdProtocolMessageType;
import com.io7m.idstore.server.security.IdSecActionType;
import com.io7m.idstore.server.security.IdSecPolicyResultDenied;
import com.io7m.idstore.server.security.IdSecurity;
import com.io7m.idstore.server.security.IdSecurityException;
import com.io7m.idstore.server.service.clock.IdServerClock;
import com.io7m.idstore.server.service.sessions.IdSessionType;
import com.io7m.idstore.server.service.telemetry.api.IdServerTelemetryServiceType;
import com.io7m.idstore.strings.IdStringConstantType;
import com.io7m.idstore.strings.IdStringConstants;
import com.io7m.idstore.strings.IdStrings;
import com.io7m.repetoir.core.RPServiceDirectoryType;
import io.opentelemetry.api.trace.Tracer;

import java.time.OffsetDateTime;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;

import static com.io7m.idstore.error_codes.IdStandardErrorCodes.HTTP_PARAMETER_INVALID;
import static com.io7m.idstore.error_codes.IdStandardErrorCodes.MAIL_SYSTEM_FAILURE;
import static com.io7m.idstore.error_codes.IdStandardErrorCodes.SECURITY_POLICY_DENIED;

/**
 * The context for execution of a command (or set of commands in a
 * transaction).
 *
 * @param <E> The type of error messages
 * @param <S> The type of sessions
 */

public abstract class IdCommandContext<E extends IdProtocolMessageType, S extends IdSessionType>
{
  private final RPServiceDirectoryType services;
  private final UUID requestId;
  private final IdDatabaseTransactionType transaction;
  private final IdServerClock clock;
  private final IdStrings strings;
  private final S session;
  private final String remoteHost;
  private final String remoteUserAgent;
  private final Tracer tracer;

  /**
   * The context for execution of a command (or set of commands in a
   * transaction).
   *
   * @param inServices        The service directory
   * @param inRequestId       The request ID
   * @param inTransaction     The transaction
   * @param inSession         The user session
   * @param inRemoteHost      The remote remoteHost
   * @param inRemoteUserAgent The remote user agent
   */

  protected IdCommandContext(
    final RPServiceDirectoryType inServices,
    final UUID inRequestId,
    final IdDatabaseTransactionType inTransaction,
    final S inSession,
    final String inRemoteHost,
    final String inRemoteUserAgent)
  {
    this.services =
      Objects.requireNonNull(inServices, "services");
    this.requestId =
      Objects.requireNonNull(inRequestId, "requestId");
    this.transaction =
      Objects.requireNonNull(inTransaction, "transaction");
    this.session =
      Objects.requireNonNull(inSession, "inSession");
    this.remoteHost =
      Objects.requireNonNull(inRemoteHost, "remoteHost");
    this.remoteUserAgent =
      Objects.requireNonNull(inRemoteUserAgent, "remoteUserAgent");

    this.clock =
      inServices.requireService(IdServerClock.class);
    this.strings =
      inServices.requireService(IdStrings.class);
    this.tracer =
      inServices.requireService(IdServerTelemetryServiceType.class)
        .tracer();
  }

  /**
   * @return The user session
   */

  public final S session()
  {
    return this.session;
  }

  /**
   * @return The remote remoteHost
   */

  public final String remoteHost()
  {
    return this.remoteHost;
  }

  /**
   * @return The remote user agent
   */

  public final String remoteUserAgent()
  {
    return this.remoteUserAgent;
  }

  /**
   * @return The service directory used during execution
   */

  public final RPServiceDirectoryType services()
  {
    return this.services;
  }

  /**
   * @return The ID of the incoming request
   */

  public final UUID requestId()
  {
    return this.requestId;
  }

  /**
   * @return The database transaction
   */

  public final IdDatabaseTransactionType transaction()
  {
    return this.transaction;
  }

  /**
   * @return The OpenTelemetry tracer
   */

  public final Tracer tracer()
  {
    return this.tracer;
  }

  /**
   * @return The current time
   */

  public final OffsetDateTime now()
  {
    return this.clock.now();
  }

  /**
   * Produce an exception indicating a database error.
   *
   * @param e The database exception
   *
   * @return An execution failure
   */

  public final IdCommandExecutionFailure failDatabase(
    final IdDatabaseException e)
  {
    return new IdCommandExecutionFailure(
      e.getMessage(),
      e,
      e.errorCode(),
      e.attributes(),
      e.remediatingAction(),
      this.requestId,
      500
    );
  }

  /**
   * Produce an exception indicating a security policy error.
   *
   * @param e The security exception
   *
   * @return An execution failure
   */

  public final IdCommandExecutionFailure failSecurity(
    final IdSecurityException e)
  {
    return new IdCommandExecutionFailure(
      e.getMessage(),
      e,
      e.errorCode(),
      e.attributes(),
      e.remediatingAction(),
      this.requestId,
      500
    );
  }

  /**
   * Produce an exception indicating a mail system error.
   *
   * @param email The email address
   * @param e     The mail system exception
   *
   * @return An execution failure
   */

  public final IdCommandExecutionFailure failMail(
    final IdEmail email,
    final Exception e)
  {
    return new IdCommandExecutionFailure(
      this.strings.format(
        IdStringConstants.MAIL_SYSTEM_FAILURE,
        email,
        e.getMessage()
      ),
      e,
      MAIL_SYSTEM_FAILURE,
      Map.of(),
      Optional.empty(),
      this.requestId,
      500
    );
  }

  /**
   * Produce an exception indicating a validation error.
   *
   * @param e The exception
   *
   * @return An execution failure
   */

  public final IdCommandExecutionFailure failValidity(
    final IdValidityException e)
  {
    return new IdCommandExecutionFailure(
      e.getMessage(),
      e,
      HTTP_PARAMETER_INVALID,
      Map.of(),
      Optional.empty(),
      this.requestId,
      400
    );
  }

  /**
   * Produce an exception indicating a password format error.
   *
   * @param e The exception
   *
   * @return An execution failure
   */

  public final IdCommandExecutionFailure failPassword(
    final IdPasswordException e)
  {
    return new IdCommandExecutionFailure(
      e.getMessage(),
      e,
      e.errorCode(),
      e.attributes(),
      e.remediatingAction(),
      this.requestId,
      400
    );
  }

  /**
   * Produce an exception indicating a protocol error.
   *
   * @param e The exception
   *
   * @return An execution failure
   */

  public final IdCommandExecutionFailure failProtocol(
    final IdProtocolException e)
  {
    return new IdCommandExecutionFailure(
      e.getMessage(),
      e,
      e.errorCode(),
      e.attributes(),
      e.remediatingAction(),
      this.requestId,
      400
    );
  }

  /**
   * Check the given action against the security policy.
   *
   * @param action The action
   *
   * @throws IdSecurityException On errors
   */

  public final void securityCheck(
    final IdSecActionType action)
    throws IdSecurityException
  {
    if (IdSecurity.check(action) instanceof final IdSecPolicyResultDenied denied) {
      throw new IdSecurityException(
        denied.message(),
        SECURITY_POLICY_DENIED,
        Map.of(),
        Optional.empty()
      );
    }
  }

  /**
   * Fail with a constant error message.
   *
   * @param statusCode The status code
   * @param errorCode  The error code
   * @param text       The message
   *
   * @return An exception
   */

  public final IdCommandExecutionFailure fail(
    final int statusCode,
    final IdErrorCode errorCode,
    final String text)
  {
    return new IdCommandExecutionFailure(
      text,
      errorCode,
      Map.of(),
      Optional.empty(),
      this.requestId,
      statusCode
    );
  }

  /**
   * Fail with a constant error message.
   *
   * @param e          The exception
   * @param statusCode The status code
   * @param errorCode  The error code
   * @param text       The message
   *
   * @return An exception
   */

  public final IdCommandExecutionFailure failExceptional(
    final Exception e,
    final int statusCode,
    final IdErrorCode errorCode,
    final String text)
  {
    return new IdCommandExecutionFailure(
      text,
      e,
      errorCode,
      Map.of(),
      Optional.empty(),
      this.requestId,
      statusCode
    );
  }

  /**
   * Fail with a constant error message.
   *
   * @param statusCode The status code
   * @param errorCode  The error code
   * @param text       The message
   * @param args       The format arguments
   *
   * @return An exception
   */

  public final IdCommandExecutionFailure failFormatted(
    final int statusCode,
    final IdErrorCode errorCode,
    final IdStringConstantType text,
    final Object... args)
  {
    return new IdCommandExecutionFailure(
      this.strings.format(text, args),
      errorCode,
      Map.of(),
      Optional.empty(),
      this.requestId,
      statusCode
    );
  }

  @Override
  public final String toString()
  {
    return "[%s 0x%s]".formatted(
      this.getClass().getSimpleName(),
      Integer.toUnsignedString(this.hashCode(), 16)
    );
  }
}