hq/app/db/IamRemediationDb.scala (153 lines of code) (raw):

package db import db.IamRemediationDb.{deserialiseIamRemediationActivity, lookupScanRequest, writePutRequest} import model._ import org.joda.time.DateTime import utils.attempt.{Attempt, Failure} import scala.jdk.CollectionConverters._ import scala.concurrent.ExecutionContext import scala.util.control.NonFatal import software.amazon.awssdk.services.dynamodb.DynamoDbClient import software.amazon.awssdk.services.dynamodb.model._ class IamRemediationDb(client: DynamoDbClient) { /** * Searches for all notification activity for this IAM user. * The application can then filter it down to relevant notifications. */ def lookupIamUserNotifications(IAMUser: IAMUser, awsAccount: AwsAccount, tableName: String)(implicit ec: ExecutionContext): Attempt[List[IamRemediationActivity]] = { for { result <- scan(lookupScanRequest(IAMUser.username, awsAccount.id, tableName)) activities <- Attempt.traverse(result)(deserialiseIamRemediationActivity) } yield activities } /** * Writes this record to the database, returning the successful DynamoDB request ID or a failure. */ def writeRemediationActivity(iamRemediationActivity: IamRemediationActivity, tableName: String)(implicit ec: ExecutionContext): Attempt[String] = { for { result <- put(writePutRequest(iamRemediationActivity, tableName)) } yield result.responseMetadata.requestId } private def scan(request: ScanRequest)(implicit ec: ExecutionContext): Attempt[List[Map[String, AttributeValue]]] = { try { Attempt.Right(client.scan(request).items.asScala.toList.map(_.asScala.toMap)) } catch { case NonFatal(e) => Attempt.Left( Failure( s"unable to scan dynamoDB table", s"I haven't been able to scan the dynamo table for the vulnerable user job", 500, throwable = Some(e) ) ) } } private def get(request: GetItemRequest)(implicit ec: ExecutionContext): Attempt[Map[String, AttributeValue]] = { try { Attempt.Right(client.getItem(request).item.asScala.toMap) } catch { case NonFatal(e) => Attempt.Left( Failure( s"unable to get item from dynamoDB table", s"I haven't been able to get the item you were looking for from the dynamo table for the vulnerable user job", 500, throwable = Some(e) ) ) } } private def put(request: PutItemRequest)(implicit ec: ExecutionContext): Attempt[PutItemResponse] = { try { Attempt.Right(client.putItem(request)) } catch { case NonFatal(e) => Attempt.Left( Failure(s"unable to put item to dynamoDB table", s"I haven't been able to put the item into the dynamo table for the vulnerable user job", 500, throwable = Some(e) ) ) } } } object IamRemediationDb { private[db] def S(str: String) = AttributeValue.builder.s(str).build() private[db] def L(list: List[AttributeValue]) = AttributeValue.builder.l(list.asJava).build() private[db] def N(number: Long) = AttributeValue.builder.n(number.toString).build() private[db] def N(number: Double) = AttributeValue.builder.n(number.toString).build() private[db] def B(boolean: Boolean) = AttributeValue.builder.bool(boolean).build() private[db] def M(map: Map[String, AttributeValue]) = AttributeValue.builder.m(map.asJava).build() private[db] def lookupScanRequest(username: String, accountId: String, tableName: String): ScanRequest = { ScanRequest.builder.tableName(tableName) .filterExpression("id = :key") .expressionAttributeValues(Map(":key" -> S(s"${accountId}/${username}")).asJava) .build() } private[db] def writePutRequest(iamRemediationActivity: IamRemediationActivity, tableName: String): PutItemRequest = { val awsAcountId = iamRemediationActivity.awsAccountId val username = iamRemediationActivity.username val dateNotificationSent = iamRemediationActivity.dateNotificationSent val iamRemediationActivityType = iamRemediationActivity.iamRemediationActivityType val iamProblem = iamRemediationActivity.iamProblem val problemCreationDate = iamRemediationActivity.problemCreationDate val iamRemediationActivityTypeString = iamRemediationActivityType match { case Warning => "Warning" case FinalWarning => "FinalWarning" case Remediation => "Remediation" } val iamProblemString = iamProblem match { case OutdatedCredential => "OutdatedCredential" } val item = Map( "id" -> S(s"${awsAcountId}/${username}"), "awsAccountId" -> S(awsAcountId), "username" -> S(username), "dateNotificationSent" -> N(dateNotificationSent.getMillis), "iamRemediationActivityType" -> S(iamRemediationActivityTypeString), "iamProblem" -> S(iamProblemString), "problemCreationDate" -> N(problemCreationDate.getMillis) ) PutItemRequest.builder.tableName(tableName).item(item.asJava).build() } /** * Attempts to deserialise a database query result into our case class. */ private[db] def deserialiseIamRemediationActivity(dbData: Map[String, AttributeValue])(implicit ec: ExecutionContext): Attempt[IamRemediationActivity] = { for { awsAccountId <- valueFromDbData(dbData, "awsAccountId", _.s) username <- valueFromDbData(dbData, "username", _.s) dateNotificationSent <- valueFromDbData(dbData, "dateNotificationSent", _.n.toLong) iamRemediationActivityTypeString <- valueFromDbData(dbData, "iamRemediationActivityType", _.s) iamRemediationActivity <- iamRemediationActivityFromString(iamRemediationActivityTypeString) iamProblemString <- valueFromDbData(dbData, "iamProblem", _.s) iamProblem <- iamProblemFromString(iamProblemString) problemCreationDate <- valueFromDbData(dbData, "problemCreationDate", _.n.toLong) } yield { IamRemediationActivity(awsAccountId, username, new DateTime(dateNotificationSent), iamRemediationActivity, iamProblem, new DateTime(problemCreationDate)) } } private def valueFromDbData[A](dbData: Map[String, AttributeValue], key: String, f: AttributeValue => A): Attempt[A] = Attempt.fromOption( dbData.get(key).flatMap(data => Option(f(data))), Failure( s"The item retrieved from the database with id ${dbData.get("id")} has an invalid attribute with key $key", s"Failed to deserialise database item into an IamRemediationActivity object", 500, None).attempt ) def iamRemediationActivityFromString(str: String): Attempt[IamRemediationActivityType] = { str match { case "Warning" => Attempt.Right(Warning) case "FinalWarning" => Attempt.Right(FinalWarning) case "Remediation" => Attempt.Right(Remediation) case _ => Attempt.Left { Failure(s"Could not turn $str to a IamRemediationActivityType", "Did not understand database record", 500).attempt } } } def iamProblemFromString(str: String): Attempt[IamProblem] = { str match { case "OutdatedCredential" => Attempt.Right(OutdatedCredential) case _ => Attempt.Left { Failure(s"Could not turn $str to an IamProblem", "Did not understand database record", 500).attempt } } } }