hq/app/aws/iam/IAMClient.scala (138 lines of code) (raw):

package aws.iam import aws.AwsAsyncHandler._ import aws.cloudformation.CloudFormation import aws.{AwsAsyncHandler, AwsClient, AwsClients} import logic.{CredentialsReportDisplay, Retry} import model.{AwsAccount, CredentialActive, CredentialDisabled, CredentialMetadata, CredentialReportDisplay, HumanUser, IAMCredential, IAMCredentialsReport, IAMUser, Tag} import org.joda.time.DateTime import play.api.Logging import utils.attempt.{Attempt, FailedAttempt, Failure} import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} import scala.jdk.CollectionConverters._ import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.iam.IamAsyncClient import software.amazon.awssdk.services.iam.model.{DeleteLoginProfileRequest, DeleteLoginProfileResponse, GenerateCredentialReportRequest, GenerateCredentialReportResponse, GetCredentialReportRequest, ListAccessKeysRequest, ListAccessKeysResponse, ListUserTagsRequest, NoSuchEntityException, UpdateAccessKeyRequest, UpdateAccessKeyResponse, StatusType} import software.amazon.awssdk.services.cloudformation.CloudFormationAsyncClient import software.amazon.awssdk.services.s3.S3AsyncClient object IAMClient extends Logging { val SOLE_REGION = Region.of("us-east-1") private def generateCredentialsReport(client: AwsClient[IamAsyncClient])(implicit ec: ExecutionContext): Attempt[GenerateCredentialReportResponse] = { val request = GenerateCredentialReportRequest.builder.build() handleAWSErrs(client)(asScala(client.client.generateCredentialReport(request))) } private def getCredentialsReport(client: AwsClient[IamAsyncClient])(implicit ec: ExecutionContext): Attempt[IAMCredentialsReport] = { val request = GetCredentialReportRequest.builder.build() handleAWSErrs(client)(asScala(client.client.getCredentialReport(request))).flatMap(CredentialsReport.extractReport) } /** * Attempts to update 'credential' with tags fetched from AWS. If the request to AWS fails, return the original credential * @return Updated or original credential */ private def enrichCredentialWithTags(credential: IAMCredential, client: AwsClient[IamAsyncClient])(implicit ec: ExecutionContext) = { val request = ListUserTagsRequest.builder.userName(credential.user).build() val result = asScala(client.client.listUserTags(request)) result.map { tagsResult => val tagsList = tagsResult.tags.asScala.toList.map(t => Tag(t.key, t.value)) credential.copy(tags = tagsList) } // If the request to fetch tags fails, just return the original user .recover { case error => logger.warn(s"Failed to fetch tags for user ${credential.user}. Storing user without tags.", error) credential } } private def enrichReportWithTags(report: IAMCredentialsReport, client: AwsClient[IamAsyncClient])(implicit ec: ExecutionContext): Attempt[IAMCredentialsReport] = { val updatedEntries = Future.sequence(report.entries.map(e => { // the root user isn't a normal IAM user - exclude from tag lookup if (!IAMCredential.isRootUser(e.user)) { enrichCredentialWithTags(e, client) } else Future.successful(e) })) val updatedReport = updatedEntries.map(e => report.copy(entries = e)) // Convert to an Attempt Attempt.fromFuture(updatedReport){ case throwable => Failure(throwable.getMessage, "failed to enrich report with tags", 500, throwable = Some(throwable)).attempt } } def getCredentialReportDisplay( account: AwsAccount, currentData: Either[FailedAttempt, CredentialReportDisplay], cfnClients: AwsClients[CloudFormationAsyncClient], iamClients: AwsClients[IamAsyncClient], regions: List[Region] )(implicit ec: ExecutionContext): Attempt[CredentialReportDisplay] = { val delay = 3.seconds val now = DateTime.now() if(CredentialsReport.credentialsReportReadyForRefresh(currentData, now)) for { client <- iamClients.get(account, SOLE_REGION) _ <- Retry.until(generateCredentialsReport(client), CredentialsReport.isComplete, "Failed to generate credentials report", delay) report <- getCredentialsReport(client) stacks <- CloudFormation.getStacksFromAllRegions(account, cfnClients, regions) reportWithTags <- enrichReportWithTags(report, client) reportWithStacks = CredentialsReport.enrichReportWithStackDetails(reportWithTags, stacks) } yield CredentialsReportDisplay.toCredentialReportDisplay(reportWithStacks) else Attempt.fromEither(currentData) } def getAllCredentialReports( accounts: Seq[AwsAccount], currentData: Map[AwsAccount, Either[FailedAttempt, CredentialReportDisplay]], cfnClients: AwsClients[CloudFormationAsyncClient], iamClients: AwsClients[IamAsyncClient], regions: List[Region] )(implicit executionContext: ExecutionContext): Attempt[Seq[(AwsAccount, Either[FailedAttempt, CredentialReportDisplay])]] = { Attempt.Async.Right { Future.traverse(accounts) { account => getCredentialReportDisplay(account, currentData(account), cfnClients, iamClients, regions).asFuture.map(account -> _) } } } def listUserAccessKeys(account: AwsAccount, user: IAMUser, iamClients: AwsClients[IamAsyncClient])(implicit ec: ExecutionContext): Attempt[List[CredentialMetadata]] = { for { client <- iamClients.get(account, SOLE_REGION) result <- listAccessKeys(client, user) keyMetdatas = result.accessKeyMetadata.asScala.toList credentialMetadatas <- Attempt.traverse(keyMetdatas) { akm => for { credentialStatus <- akm.status match { case StatusType.ACTIVE => Attempt.Right (CredentialActive) case StatusType.INACTIVE => Attempt.Right (CredentialDisabled) case StatusType.UNKNOWN_TO_SDK_VERSION => Attempt.Left { Failure ( s"Could not create credential metadata from status value, as it is unknown to SDK version (expected 'Active' or 'Inactive')", "Couldn't lookup AWS Access Key metadata", 500 ) } } } yield CredentialMetadata(akm.userName, akm.accessKeyId, new DateTime(akm.createDate), credentialStatus) } } yield credentialMetadatas } private def listAccessKeys(client: AwsClient[IamAsyncClient], user: IAMUser)(implicit ec: ExecutionContext): Attempt[ListAccessKeysResponse] = { val request = ListAccessKeysRequest.builder.userName(user.username).build() handleAWSErrs(client)(asScala(client.client.listAccessKeys(request))) } def disableAccessKey(awsAccount: AwsAccount, username: String, accessKeyId: String, iamClients: AwsClients[IamAsyncClient])(implicit ec: ExecutionContext): Attempt[UpdateAccessKeyResponse] = { val request = UpdateAccessKeyRequest.builder .userName(username) .accessKeyId(accessKeyId) .status("Inactive") .build() for { client <- iamClients.get(awsAccount, SOLE_REGION) result <- handleAWSErrs(client)(asScala(client.client.updateAccessKey(request))) } yield result } private def handleDeleteLoginProfileErrs(awsClient: AwsClient[IamAsyncClient], username: String)(f: => Future[DeleteLoginProfileResponse])(implicit ec: ExecutionContext): Attempt[Option[DeleteLoginProfileResponse]] = AwsAsyncHandler.handleAWSErrs(awsClient)(f.map(Some.apply).recover({ case e if e.getMessage.contains(s"Login Profile for User $username cannot be found") => None case _: NoSuchEntityException => None })) def deleteLoginProfile(awsAccount: AwsAccount, username: String, iamClients: AwsClients[IamAsyncClient])(implicit ec: ExecutionContext): Attempt[Option[DeleteLoginProfileResponse]] = { val request = DeleteLoginProfileRequest.builder.userName(username).build() for { client <- iamClients.get(awsAccount, SOLE_REGION) result <- handleDeleteLoginProfileErrs(client, username)(asScala(client.client.deleteLoginProfile(request))) } yield result } }