app/services/DynamoPermissionsCache.scala (77 lines of code) (raw):

package services import com.typesafe.scalalogging.StrictLogging import io.circe.{Decoder, Encoder} import io.circe.generic.extras.Configuration import io.circe.generic.extras.semiauto.{deriveEnumerationDecoder, deriveEnumerationEncoder} import io.circe.generic.auto._ import models.DynamoErrors.DynamoGetError import services.UserPermissions.{PagePermission, UserPermissions, decoder} import software.amazon.awssdk.services.dynamodb.DynamoDbClient import software.amazon.awssdk.services.dynamodb.model.{AttributeValue, ScanRequest} import utils.Circe.dynamoMapToJson import zio.blocking.effectBlocking import zio.duration.durationInt import zio.{Schedule, ZEnv, ZIO} import scala.jdk.CollectionConverters._ import java.util.concurrent.atomic.AtomicReference object UserPermissions { // The model for the user permissions that we store in DynamoDb sealed trait Permission object Permission { case object Read extends Permission case object Write extends Permission implicit val customConfig: Configuration = Configuration.default.withDefaults implicit val decoder: Decoder[Permission] = deriveEnumerationDecoder[Permission] implicit val encoder: Encoder[Permission] = deriveEnumerationEncoder[Permission] } case class PagePermission(name: String, permission: Permission) case class UserPermissions(email: String, permissions: List[PagePermission]) implicit val encoder = Encoder[UserPermissions] implicit val decoder = Decoder[UserPermissions] } /** * Polls the user permissions DynamoDb table and caches all permissions in memory */ class DynamoPermissionsCache( stage: String, client: DynamoDbClient, runtime: zio.Runtime[ZEnv] ) extends DynamoService(stage, client) with StrictLogging { type Email = String protected val tableName = s"support-admin-console-permissions-$stage" private val permissionsCache = new AtomicReference[Map[Email, UserPermissions]](Map.empty) private def getAll: ZIO[ZEnv, DynamoGetError, java.util.List[java.util.Map[String, AttributeValue]]] = effectBlocking { client .scan( ScanRequest .builder() .tableName(tableName) .build() ) .items() }.mapError(DynamoGetError) private def fetchPermissions(): ZIO[ZEnv, DynamoGetError, Map[Email, UserPermissions]] = getAll.map( results => results.asScala .map(item => dynamoMapToJson(item).as[UserPermissions]) .flatMap { case Right(userPermissions) => Some(userPermissions) case Left(error) => logger.error( s"Failed to decode UserPermissions from Dynamo: ${error.getMessage}") None } .map(userPermissions => userPermissions.email -> userPermissions) .toMap ) private def updatePermissions(permissions: Map[Email, UserPermissions]) = { permissionsCache.set(permissions) ZIO.succeed() } // Poll every minute in the background runtime.unsafeRunAsync_ { fetchPermissions() .map(updatePermissions) .repeat(Schedule.fixed(1.minute)) } def getPermissionsForUser(email: Email): Option[List[PagePermission]] = { permissionsCache.get().get(email).map(_.permissions) } }