Skip to content

Commit

Permalink
Move ARN extractiors to fromArnString objects
Browse files Browse the repository at this point in the history
  • Loading branch information
sattvik committed May 26, 2016
1 parent ba7d512 commit 9bc34c4
Show file tree
Hide file tree
Showing 48 changed files with 293 additions and 186 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ class AccountArnSpec extends FreeSpec {

"round-trip via an ARN string" in {
forAll { arn: AccountArn
AccountArn(arn.arnString) shouldBe arn
AccountArn.fromArnString(arn.arnString) shouldBe arn
}
}

"will fail to parse an invalid ARN" in {
an [IllegalArgumentException] shouldBe thrownBy {
AccountArn("arn:aws:iam::111122223333:user/foo")
AccountArn.fromArnString("arn:aws:iam::111122223333:user/foo")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,19 @@ class ArnSpec extends FreeSpec {
"have an extractor that returns" - {
"a generic result when no applicable partial function has been registered" in {
forAll { arn: TestArn
Arn.unapply(arn.arnString) shouldBe
Some(Arn.GenericArn(arn.testPartition, arn.testNamespace, arn.testRegion, arn.testAccount, arn.testResource))
Arn.fromArnString(arn.arnString) shouldBe
Arn.GenericArn(arn.testPartition, arn.testNamespace, arn.testRegion, arn.testAccount, arn.testResource)
}
}

"a specfic result once a partial function has been registered" in {
val testMatcher: PartialFunction[Arn.ArnParts, TestArn] = {
case (partition, namespace, region, account, resource) TestArn(partition, namespace, region, account, resource)
}
Arn.registerArnMatchers(testMatcher)
Arn.registerArnPartialFunctions(testMatcher)

forAll { arn: TestArn
Arn.unapply(arn.arnString) shouldBe Some(arn)
Arn.fromArnString(arn.arnString) shouldBe arn
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ class RoleArnSpec extends FreeSpec {

"can round-trip via an ARN" in {
forAll { arn: RoleArn
RoleArn(arn.arnString) shouldBe arn
RoleArn.fromArnString(arn.arnString) shouldBe arn
}
}

"will fail to parse an invalid ARN" in {
an [IllegalArgumentException] shouldBe thrownBy {
RoleArn("arn:aws:iam::111222333444:root")
RoleArn.fromArnString("arn:aws:iam::111222333444:root")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ class SamlProviderArnSpec extends FreeSpec {

"round-trip via an ARN" in {
forAll { arn: SamlProviderArn
Arn.unapply(arn.arnString) shouldBe Some(arn)
SamlProviderArn.fromArnString(arn.arnString) shouldBe arn
}
}

"will fail to parse an invalid ARN" in {
an [IllegalArgumentException] shouldBe thrownBy {
SamlProviderArn.fromArnString("arn:aws:iam::111222333444:root")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ class UserArnSpec extends FreeSpec {

"can round-trip via an ARN" in {
forAll { arn: UserArn
UserArn(arn.arnString) shouldBe arn
UserArn.fromArnString(arn.arnString) shouldBe arn
}
}

"will fail to parse an invalid ARN" in {
an [IllegalArgumentException] shouldBe thrownBy {
UserArn("arn:aws:iam::111222333444:root")
UserArn.fromArnString("arn:aws:iam::111222333444:root")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ class AssumedRoleArnSpec extends FreeSpec {

"can round-trip via an ARN" in {
forAll { arn: AssumedRoleArn
AssumedRoleArn(arn.arnString) shouldBe arn
AssumedRoleArn.fromArnString(arn.arnString) shouldBe arn
}
}

"will fail to parse an invalid ARN" in {
an [IllegalArgumentException] shouldBe thrownBy {
AssumedRoleArn("arn:aws:iam::111222333444:root")
AssumedRoleArn.fromArnString("arn:aws:iam::111222333444:root")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@ case class AccountArn(account: Account) extends Arn(account.partition, Arn.Names
}

object AccountArn {
/** Builds an account ARN object from the given ARN string. */
def apply(arnString: String): AccountArn =
arnString match {
case Arn(accountArn: AccountArn) accountArn
case _ throw new IllegalArgumentException(s"$arnString’ is not a valid account ARN.")
}
/** Utility to build/extract `AccountArn` instances from strings. */
object fromArnString {
/** Builds an `AccountArn` object from the given ARN string. */
def apply(arnString: String): AccountArn =
unapply(arnString).getOrElse(throw new IllegalArgumentException(s"$arnString’ is not a valid account ARN."))

/** Extracts an `AccountArn` object from the given ARN string. */
def unapply(arnString: String): Option[AccountArn] =
arnString match {
case Arn.fromArnString(accountArn: AccountArn) Some(accountArn)
case _ None
}
}

private[awsutil] val accountArnPF: PartialFunction[Arn.ArnParts, AccountArn] = {
case (_, Arn.Namespace.IAM, None, Some(account), "root") AccountArn(account)
Expand Down
93 changes: 52 additions & 41 deletions aws2scala-core/src/main/scala/com/monsanto/arch/awsutil/Arn.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ object Arn {
private[awsutil] type ArnParts = (Partition, Arn.Namespace, Option[Region], Option[Account], String)

/** The set of all possible partial functions that can extract an `Arn` subclass given the components of an ARN. */
private val subextractors: mutable.Set[PartialFunction[ArnParts,Arn]] =
private val arnPartialFunctions: mutable.Set[PartialFunction[ArnParts,Arn]] =
mutable.LinkedHashSet(
AccountArn.accountArnPF,
AssumedRoleArn.assumeRoleArnPF,
Expand All @@ -55,49 +55,12 @@ object Arn {
)

/** Registers partial functions that can be used to extract `Arn` subclasses given a set of ARN parts. */
private[awsutil] def registerArnMatchers(matchers: PartialFunction[ArnParts,Arn]*): Unit = {
subextractors.synchronized {
subextractors ++= matchers
private[awsutil] def registerArnPartialFunctions(partialFunctions: PartialFunction[ArnParts,Arn]*): Unit = {
arnPartialFunctions.synchronized {
arnPartialFunctions ++= partialFunctions
}
}

/** Given a string, extract an ARN object instance. */
def unapply(str: String): Option[Arn] = {
// first, try to get the parts of the ARN
val maybeArnParts = str match {
// no region or account
case ArnRegex(Partition(partition), Arn.Namespace.fromId(namespace), "", "", resource)
Some((partition, namespace, None, None, resource))

// region, but no account
case ArnRegex(Partition(partition), Arn.Namespace.fromId(namespace), Region(region), "", resource)
Some((partition, namespace, Some(region), None, resource))

// account, but no region
case ArnRegex(Partition(partition), Arn.Namespace.fromId(namespace), "", accountId, resource)
Some((partition, namespace, None, Some(Account(accountId, partition)), resource))

// account and region
case ArnRegex(Partition(partition), Arn.Namespace.fromId(namespace), Region(region), accountId, resource)
Some((partition, namespace, Some(region), Some(Account(accountId, partition)), resource))

case _ None
}
// now, extract an arn by finding it in the registered extractors, ending with GenericArn if no extractors
// are found.
maybeArnParts.flatMap { arnParts
subextractors.synchronized {
subextractors.view
.filter(_.isDefinedAt(arnParts))
.map(_.apply(arnParts))
.headOption
.orElse(Some((GenericArn.apply _).tupled.apply(arnParts)))
}
}
}

/** Regular expression to match the parts of a regular expression. */
private val ArnRegex = "^arn:([^:]+):([^:]+):([^:]*):([^:]*):(.+)$".r

/** Generic ARN subclass that may be used when no registered matchers match a parsed ARN. */
private[awsutil] case class GenericArn(_partition: Partition,
Expand All @@ -109,6 +72,54 @@ object Arn {
/** Enumerated type for ARN namespaces. */
sealed abstract class Namespace(val id: String)

/** Utility to extract/build an `Arn` instance from a string containing an ARN. */
object fromArnString {
def apply(arnString: String): Arn =
unapply(arnString).getOrElse(throw new IllegalArgumentException(s"$arnString‘ is not a valid ARN."))

/** Given a string, extract an ARN object instance. This extractor will attempt to return an `Arn` subclass
* that is specific to the ARN (if such a subclass has registered an applicable partial function). If no
* such registered partial function exists, but the string is still an ARN, a generic ARN instance will
* be returned.
*/
def unapply(arnString: String): Option[Arn] = {
// first, try to get the parts of the ARN
val maybeArnParts = arnString match {
// no region or account
case ArnRegex(Partition(partition), Arn.Namespace.fromId(namespace), "", "", resource)
Some((partition, namespace, None, None, resource))

// region, but no account
case ArnRegex(Partition(partition), Arn.Namespace.fromId(namespace), Region(region), "", resource)
Some((partition, namespace, Some(region), None, resource))

// account, but no region
case ArnRegex(Partition(partition), Arn.Namespace.fromId(namespace), "", accountId, resource)
Some((partition, namespace, None, Some(Account(accountId, partition)), resource))

// account and region
case ArnRegex(Partition(partition), Arn.Namespace.fromId(namespace), Region(region), accountId, resource)
Some((partition, namespace, Some(region), Some(Account(accountId, partition)), resource))

case _ None
}
// now, extract an arn by finding it in the registered extractors, ending with GenericArn if no extractors
// are found.
maybeArnParts.flatMap { arnParts
arnPartialFunctions.synchronized {
arnPartialFunctions.view
.filter(_.isDefinedAt(arnParts))
.map(_.apply(arnParts))
.headOption
.orElse(Some((GenericArn.apply _).tupled.apply(arnParts)))
}
}
}

/** Regular expression to match the parts of a regular expression. */
private val ArnRegex = "^arn:([^:]+):([^:]+):([^:]*):([^:]*):(.+)$".r
}

//noinspection SpellCheckingInspection
object Namespace {
case object ApiGateway extends Namespace("apigateway")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.monsanto.arch.awsutil.auth.policy

import com.monsanto.arch.awsutil.identitymanagement.model.{RoleArn, SamlProviderArn, UserArn}
import com.monsanto.arch.awsutil.securitytoken.model.AssumedRoleArn
import com.monsanto.arch.awsutil.{Account, AccountArn, Arn}
import com.monsanto.arch.awsutil.{Account, AccountArn}

/** A principal specifies the user (IAM user, federated user, or assumed-role user), AWS account, AWS service, or other
* principal entity that it allowed or denied access to a resource.
Expand Down Expand Up @@ -132,17 +132,17 @@ object Principal {
Some(Principal.allWebProviders)
case ("Federated", Principal.WebIdentityProvider.fromProvider(webIdentityProvider))
Some(Principal.webProvider(webIdentityProvider))
case ("Federated", Arn(samlProviderArn: SamlProviderArn))
case ("Federated", SamlProviderArn.fromArnString(samlProviderArn))
Some(Principal.SamlProviderPrincipal(samlProviderArn))
case ("AWS", Arn(AccountArn(account)))
case ("AWS", AccountArn.fromArnString(AccountArn(account)))
Some(Principal.AccountPrincipal(account))
case ("AWS", Account.fromNumber(account))
Some(Principal.AccountPrincipal(account))
case ("AWS", Arn(userArn: UserArn))
case ("AWS", UserArn.fromArnString(userArn))
Some(Principal.IamUserPrincipal(userArn))
case ("AWS", Arn(roleArn: RoleArn))
case ("AWS", RoleArn.fromArnString(roleArn))
Some(Principal.IamRolePrincipal(roleArn))
case ("AWS", Arn(assumedRoleArn: AssumedRoleArn))
case ("AWS", AssumedRoleArn.fromArnString(assumedRoleArn))
Some(Principal.StsAssumedRolePrincipal(assumedRoleArn))
case _
None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@ case class RoleArn(account: Account, name: String, path: Path = Path.empty) exte
}

object RoleArn {
/** Builds a role ARN object from the given ARN string. */
def apply(arnString: String): RoleArn =
arnString match {
case Arn(arn: RoleArn) arn
case _ throw new IllegalArgumentException(s"$arnString’ is not a valid role ARN.")
}
/** Utility to build/extract `RoleArn` instances from strings containing ARNs. */
object fromArnString {
/** Builds a `RoleArn` object from the given ARN string. */
def apply(arnString: String): RoleArn =
unapply(arnString).getOrElse(throw new IllegalArgumentException(s"$arnString’ is not a valid role ARN."))

/** Extracts a `RoleArn` object from the given ARN string. */
def unapply(arnString: String): Option[RoleArn] =
arnString match {
case Arn.fromArnString(arn: RoleArn) Some(arn)
case _ None
}
}

private[awsutil] val roleArnPF: PartialFunction[Arn.ArnParts, RoleArn] = {
case (_, Arn.Namespace.IAM, None, Some(account), RoleResourceRegex(Path.fromPathString(path), name))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ case class SamlProviderArn(account: Account, name: String) extends Arn(Arn.Names
}

private[awsutil] object SamlProviderArn {
/** Utility to build/extract `SamlProviderArn` instances from strings containing ARNs. */
object fromArnString {
/** Builds a `SamlProviderArn` object from the given ARN string. */
def apply(arnString: String): SamlProviderArn =
unapply(arnString)
.getOrElse(throw new IllegalArgumentException(s"$arnString’ is not a valid SAML provider ARN."))

/** Extracts a `SamlProviderArn` object from the given ARN string. */
def unapply(arnString: String): Option[SamlProviderArn] =
arnString match {
case Arn.fromArnString(arn: SamlProviderArn) Some(arn)
case _ None
}
}

private[awsutil] val samlProviderArnPF: PartialFunction[Arn.ArnParts, SamlProviderArn] = {
case (_, Arn.Namespace.IAM, None, Some(account), SamlProviderName(name)) SamlProviderArn(account, name)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@ case class UserArn(account: Account, name: String, path: Path = Path.empty) exte
}

object UserArn {
/** Builds a user ARN object from the given ARN string. */
def apply(arnString: String): UserArn =
arnString match {
case Arn(arn: UserArn) arn
case _ throw new IllegalArgumentException(s"$arnString’ is not a valid user ARN.")
}
/** Utility to build/extract `UserArn` instances from strings containing ARNs. */
object fromArnString {
/** Builds a `UserArn` object from the given ARN string. */
def apply(arnString: String): UserArn =
unapply(arnString).getOrElse(throw new IllegalArgumentException(s"$arnString’ is not a valid user ARN."))

/** Extracts a `UserArn` object from the given ARN string. */
def unapply(arnString: String): Option[UserArn] =
arnString match {
case Arn.fromArnString(arn: UserArn) Some(arn)
case _ None
}
}

private[awsutil] val userArnPF: PartialFunction[Arn.ArnParts, UserArn] = {
case (_, Arn.Namespace.IAM, None, Some(account), UserResourceRegex(Path.fromPathString(path), name))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,19 @@ case class AssumedRoleArn(account: Account,
}

object AssumedRoleArn {
/** Builds an assumed role ARN object from the given ARN string. */
def apply(arnString: String): AssumedRoleArn =
arnString match {
case Arn(arn: AssumedRoleArn) arn
case _ throw new IllegalArgumentException(s"$arnString’ is not a valid assumed role ARN.")
}
/** Utility to build/extract `AssumedRoleArn` instances from strings containing ARNs. */
object fromArnString {
/** Builds a `AssumedRoleArn` object from the given ARN string. */
def apply(arnString: String): AssumedRoleArn =
unapply(arnString).getOrElse(throw new IllegalArgumentException(s"$arnString’ is not a valid assumed role ARN."))

/** Extracts a `AssumedRoleArn` object from the given ARN string. */
def unapply(arnString: String): Option[AssumedRoleArn] =
arnString match {
case Arn.fromArnString(arn: AssumedRoleArn) Some(arn)
case _ None
}
}

private[awsutil] val assumeRoleArnPF: PartialFunction[Arn.ArnParts, AssumedRoleArn] = {
case (_, Arn.Namespace.AwsSTS, None, Some(account), AssumedRoleResourceRegex(roleName, sessionName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ object Ec2ScalaCheckImplicits {

implicit lazy val shrinkIamInstanceProfile: Shrink[IamInstanceProfile] =
Shrink { profile
Shrink.shrink(InstanceProfileArn(profile.arn)).map(arn profile.copy(arn = arn.arnString))
Shrink.shrink(InstanceProfileArn.fromArnString(profile.arn)).map(arn profile.copy(arn = arn.arnString))
}

implicit lazy val arbInstance: Arbitrary[Instance] = {
Expand Down

0 comments on commit 9bc34c4

Please sign in to comment.