All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.gu.play.secretrotation.DualSecretTransition.scala Maven / Gradle / Ivy

There is a newer version: 11.1.1
Show newest version
package com.gu.play.secretrotation

import java.time.Clock.systemUTC
import java.time.{Clock, Duration, Instant}
import com.gu.play.secretrotation.DualSecretTransition.Phase.{Completed, InProgress, Upcoming}
import com.gu.play.secretrotation.DualSecretTransition.Secret.{Age, New, Old}
import org.threeten.extra.Interval

import scala.math.Ordering.Implicits._


object DualSecretTransition {
  object Secret {
    sealed trait Age

    object Old extends Age
    object New extends Age
  }

  sealed trait PhaseAges extends Phase[Age]

  object Phase {

    class PhaseAge(val active: Age, val alsoAccepted: Iterable[Age] = None) extends PhaseAges

    object Upcoming   extends PhaseAge(active = Old, alsoAccepted = Some(New))
    object InProgress extends PhaseAge(active = New, alsoAccepted = Some(Old))
    object Completed  extends PhaseAge(active = New)
  }


  case class InitialSecret(secret: String) extends SnapshotProvider {

    override def snapshot(): SecretsSnapshot = new SecretsSnapshot {
      val description = "Initial secret, no known upcoming rotation of secret"

      override def decode[T](decodingFunc: String => T, conclusiveDecode: T => Boolean): Option[T] =
        Seq(decodingFunc(secret)).find(conclusiveDecode)

      override val secrets = new Phase[String] {
        val active = secret
        val alsoAccepted = Nil
      }
    }
  }


  case class TransitioningSecret(
    oldSecret: String,
    newSecret: String,
    overlapInterval: Interval
  )(implicit clock: Clock = systemUTC()) extends SnapshotProvider {

    val secretsByAge = Map(Old -> oldSecret, New -> newSecret)

    val phaseSchedule =
      PhaseSchedule(Upcoming, overlapInterval.getStart -> InProgress, overlapInterval.getEnd -> Completed)

    val overlapDuration = overlapInterval.toDuration
    val permittedSnapshotStaleness = overlapDuration.dividedBy(10)
    val warningThreshold: Instant = overlapInterval.getEnd.minus(overlapDuration.dividedBy(5))

    def snapshot(): SecretsSnapshot = {
      val snapshotTime = clock.instant()
      val snapshotBestBefore = snapshotTime.plus(permittedSnapshotStaleness)

      val phase = phaseSchedule.phaseAt(snapshotTime)

      new SecretsSnapshot {
        override def secrets: Phase[String] = phase.map(secretsByAge)

        override def description: String = phase match {
          case Upcoming =>
            s"upcoming transition between old and new secrets in ${Duration.between(snapshotTime, overlapInterval.getStart)} at ${overlapInterval.getStart}"
          case InProgress =>
            s"transition between old and new secrets in progress (during $overlapInterval)"
          case Completed =>
            s"transition to latest secret completed at ${overlapInterval.getEnd}"
        }

        /**
          * Want to know:
          * If decoding was successful, but with a legacy secret (especially if we are towards the end of the overlap period)
          */
        override def decode[T](decodingFunc: String => T, conclusiveDecode: T => Boolean): Option[T] = {
          if (clock.instant() > snapshotBestBefore)
            logger.warn("Don't hold onto snapshots! Get a new snapshot with each user interaction.")

          (for {
            secret <- secrets.accepted
            decodedValue = decodingFunc(secret)
            if conclusiveDecode(decodedValue)
          } yield {
            if (secret != secrets.active) {
              val message = s"Accepted decode with non-active key : $description"
              if (clock.instant() > warningThreshold) logger.warn(message) else logger.debug(message)
            }
            decodedValue
          }).headOption
        }
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy