hq/app/logic/IamUnrecognisedUsers.scala (121 lines of code) (raw):
package logic
import aws.AwsClients
import aws.iam.IAMClient.{deleteLoginProfile, disableAccessKey, listUserAccessKeys}
import com.gu.anghammarad.models.Notification
import com.gu.janus.model.JanusData
import logging.Cloudwatch
import logging.Cloudwatch.ReaperExecutionStatus
import model._
import notifications.AnghammaradNotifications.unrecognisedUserRemediation
import play.api.Logging
import utils.attempt.{Attempt, FailedAttempt}
import java.io.File
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import scala.concurrent.ExecutionContext
import software.amazon.awssdk.services.iam.IamAsyncClient
import software.amazon.awssdk.services.iam.model.{DeleteLoginProfileResponse, UpdateAccessKeyResponse}
object IamUnrecognisedUsers extends Logging {
val USERNAME_TAG_KEY = "GoogleUsername"
def getJanusUsernames(janusData: JanusData): List[String] =
janusData.access.userAccess.keys.toList
/**
* Removes the FailedAttempts from the Either and returns a list of tuples with only the Right values.
* This function uses generics to make it easier to test, but to avoid confusion it was written to take
* a Map of AWSAccount to Either and return a list of tuples of AWS Account to CredentialReportDisplay.
*/
def getCredsReportDisplayForAccount[A, B](allCreds: Map[A, Either[FailedAttempt, B]]): List[(A, B)] = {
allCreds.toList.foldLeft[List[(A, B)]](Nil) {
case (acc, (_, Left(failure))) =>
failure.firstException match {
case Some(cause) =>
logger.error(s"unable to generate credential report display: ${failure.logMessage}", cause)
case None =>
logger.error(s"unable to generate credential report display: ${failure.logMessage}")
}
acc
case (acc, (account, Right(credReportDisplay))) =>
(account, credReportDisplay) :: acc
}
}
/**
* Returns IAM permanent credentials for people who are not janus users.
* Filters for the accounts the Security HQ stage has been configured for - see "alert.allowedAccountIds" in configuration.
*/
def unrecognisedUsersForAllowedAccounts(
accountCredsReports: List[(AwsAccount, CredentialReportDisplay)],
janusUsernames: List[String],
allowedAccountIds: List[String]
): List[AccountUnrecognisedUsers] = {
for {
(acc, crd) <- accountCredsReports
accountUsers = AccountUnrecognisedUsers(acc, filterUnrecognisedIamUsers(crd.humanUsers, janusUsernames))
accountId = accountUsers.account.id
if accountUsers.unrecognisedUsers.nonEmpty && allowedAccountIds.contains(accountId)
} yield accountUsers
}
private def filterUnrecognisedIamUsers(iamHumanUsersWithTargetTag: Seq[HumanUser], janusUsernames: List[String]): List[HumanUser] =
iamHumanUsersWithTargetTag.filterNot { iamUser =>
val maybeTag = iamUser.tags.find(tag => tag.key == USERNAME_TAG_KEY)
maybeTag match {
case Some(tag) => janusUsernames.contains(tag.value) // filter out human users that have tags which match the janus usernames
case None => true
}
}.toList
def makeFile(s3Object: String): File = {
Files.write(
Files.createTempFile("janusData", ".txt"),
s3Object.getBytes(StandardCharsets.UTF_8)
).toFile
}
def isTaggedForUnrecognisedUser(tags: List[Tag]): Boolean = {
tags.exists(t =>
t.key == USERNAME_TAG_KEY &&
t.value != "" &&
t.value.contains(".")
)
}
def listAccountAccessKeys(
accountUnrecognisedUsers: AccountUnrecognisedUsers,
iamClients: AwsClients[IamAsyncClient]
)(implicit ec: ExecutionContext): Attempt[AccountUnrecognisedAccessKeys] = {
val AccountUnrecognisedUsers(account, users) = accountUnrecognisedUsers
Attempt.flatTraverse(users)(listUserAccessKeys(account, _, iamClients)).map {
AccountUnrecognisedAccessKeys(account, _)
}
}
def disableAccountAccessKeys(
accountUnrecognisedKeys: AccountUnrecognisedAccessKeys,
iamClients: AwsClients[IamAsyncClient]
)(implicit ec: ExecutionContext): Attempt[List[UpdateAccessKeyResponse]] = {
val AccountUnrecognisedAccessKeys(account, accessKeys) = accountUnrecognisedKeys
val activeAccessKeys = accessKeys.filter(_.status == CredentialActive)
val disableKeysAttempt = Attempt.traverse(activeAccessKeys)(key =>
disableAccessKey(account, key.username, key.accessKeyId, iamClients)
)
disableKeysAttempt.tap(_.fold(
{ failure =>
logger.error(s"Failed to disable access key: ${failure.logMessage}")
Cloudwatch.putIamDisableAccessKeyMetric(ReaperExecutionStatus.failure)
},
{ updateAccessKeyResults =>
logger.info(s"Attempt to disable access keys was successful. ${updateAccessKeyResults.length} key(s) were disabled in ${account.name}.")
if(updateAccessKeyResults.nonEmpty) {
Cloudwatch.putIamDisableAccessKeyMetric(ReaperExecutionStatus.success)
}
}
))
}
def removeAccountPasswords(
accountUnrecognisedUsers: AccountUnrecognisedUsers,
iamClients: AwsClients[IamAsyncClient]
)(implicit ec: ExecutionContext): Attempt[List[Option[DeleteLoginProfileResponse]]] = {
val results = Attempt.traverse(accountUnrecognisedUsers.unrecognisedUsers)(user => deleteLoginProfile(accountUnrecognisedUsers.account, user.username, iamClients))
results.tap {
case Left(failure) =>
logger.error(s"failed to delete at least one password: ${failure.logMessage}")
Cloudwatch.putIamRemovePasswordMetric(ReaperExecutionStatus.failure, 1)
case Right(success) =>
logger.info(s"passwords deleted for ${accountUnrecognisedUsers.unrecognisedUsers.map(_.username).mkString(",")}")
Cloudwatch.putIamRemovePasswordMetric(ReaperExecutionStatus.success, success.flatten.length)
}
}
def unrecognisedUserNotifications(accountUsers: List[AccountUnrecognisedUsers]): List[Notification] = {
accountUsers.flatMap { case AccountUnrecognisedUsers(account, users) =>
users.map { user =>
unrecognisedUserRemediation(account, user)
}
}
}
}