diff --git a/aws2scala-core-tests/src/test/scala/com/monsanto/arch/awsutil/auth/policy/PolicyJsonSupportSpec.scala b/aws2scala-core-tests/src/test/scala/com/monsanto/arch/awsutil/auth/policy/PolicyJsonSupportSpec.scala new file mode 100644 index 0000000..642dc01 --- /dev/null +++ b/aws2scala-core-tests/src/test/scala/com/monsanto/arch/awsutil/auth/policy/PolicyJsonSupportSpec.scala @@ -0,0 +1,405 @@ +package com.monsanto.arch.awsutil.auth.policy + +import com.amazonaws.auth.{policy ⇒ aws} +import com.amazonaws.util.json.JSONArray +import com.monsanto.arch.awsutil.auth.policy.PolicyJsonSupport._ +import com.monsanto.arch.awsutil.converters.CoreConverters._ +import com.monsanto.arch.awsutil.testkit.CoreScalaCheckImplicits._ +import com.monsanto.arch.awsutil.testkit.UtilGen +import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.Gen +import org.scalatest.FreeSpec +import org.scalatest.Matchers._ +import org.scalatest.prop.GeneratorDrivenPropertyChecks._ +import spray.json.{JsArray, JsNull, JsObject, JsString, JsValue, JsonParser} + +class PolicyJsonSupportSpec extends FreeSpec { + TestAction.registerActions() + + "the PolicyJsonSupport should" - { + "properly serialise" - { + "a principal set that" - { + "is empty" in { + principalsToJson(Set.empty) shouldBe None + } + + "is all principals set" in { + principalsToJson(Statement.allPrincipals) shouldBe Some("*") + } + + "contains a single principal" in { + forAll { principal: Principal ⇒ + val expected = JsObject(principal.provider → JsString(principal.id)) + val result = JsonParser(principalsToJson(Set(principal)).get.toString) + result shouldBe expected + } + } + + "contains multiple principals with the same provider" in { + val sameProviderPrincipals = + ( + for { + principal1 ← arbitrary[Principal] + principal2 ← arbitrary[Principal].retryUntil(_.provider == principal1.provider) + } yield Set(principal1, principal2) + ).suchThat(_.size == 2) + forAll(sameProviderPrincipals) { principals ⇒ + val expected = JsObject(principals.head.provider → JsArray(principals.toSeq.map(p ⇒ JsString(p.id)): _*)) + + val result = JsonParser(principalsToJson(principals).get.toString) + + result shouldBe expected + } + } + + "contains multiple principals from different providers" in { + val twoPrincipals = + for { + principal1 ← arbitrary[Principal] + principal2 ← arbitrary[Principal].retryUntil(_.provider != principal1.provider) + } yield Set(principal1, principal2) + forAll(twoPrincipals) { principals ⇒ + val expected = JsObject(principals.map(p ⇒ p.provider → JsString(p.id)).toMap) + val result = JsonParser(principalsToJson(principals).get.toString) + + result shouldBe expected + } + } + } + + "an action list that" - { + "is empty" in { + actionsToJson(Seq.empty) shouldBe None + } + + "is the all actions sequence" in { + actionsToJson(Statement.allActions) shouldBe Some("*") + } + + "contains a single action" in { + forAll { action: Action ⇒ + val result = actionsToJson(Seq(action)) + val expected = Some(action.name) + result shouldBe expected + } + } + + "contains multiple actions" in { + val genActions = + for { + first ← arbitrary[Action] + rest ← UtilGen.nonEmptyListOfSqrtN(arbitrary[Action]) + } yield first :: rest + + forAll(genActions) { actions ⇒ + val result = JsonParser(actionsToJson(actions).get.toString) + val expected = JsArray(actions.map(a ⇒ JsString(a.name)): _*) + result shouldBe expected + } + } + } + + "a resource list that" - { + "is empty" in { + resourcesToJson(Seq.empty) shouldBe None + } + + "is the all resources list" in { + resourcesToJson(Statement.allResources) shouldBe Some("*") + } + + "contains a single resource" in { + forAll { resource: Resource ⇒ + resourcesToJson(Seq(resource)) shouldBe Some(resource.id) + } + } + + "contains more than one resource" in { + val genResources = + for { + first ← arbitrary[Resource] + rest ← UtilGen.nonEmptyListOfSqrtN(arbitrary[Resource]) + } yield first :: rest + + forAll(genResources) { resources ⇒ + val result = JsonParser(resourcesToJson(resources).get.toString) + val expected = JsArray(resources.map(r ⇒ JsString(r.id)): _*) + result shouldBe expected + } + } + } + + "a condition set that" - { + "is empty" in { + conditionsToJson(Set.empty) shouldBe None + } + + "contains a single condition with a single value" - { + val singleValueCondition = + for { + condition ← arbitrary[Condition] + } yield { + Condition(condition.key, condition.comparisonType, Seq(condition.comparisonValues.head)) + } + forAll(singleValueCondition) { condition ⇒ + val expected = + JsObject( + condition.comparisonType → JsObject( + condition.key → JsString(condition.comparisonValues.head) + ) + ) + + val result = JsonParser(conditionsToJson(Set(condition)).get.toString) + + result shouldBe expected + } + } + + "contains a single condition with multiple comparison values" in { + val multiValueCondition = + for { + condition ← Gen.sized { n ⇒ + Gen.resize(n.min(10), arbitrary[Condition]).suchThat(_.comparisonValues.distinct.size > 1) + } + } yield condition + forAll(multiValueCondition) { condition ⇒ + val expected = + JsObject( + condition.comparisonType → JsObject( + condition.key → JsArray(condition.comparisonValues.distinct.map(JsString.apply): _*) + ) + ) + + val result = JsonParser(conditionsToJson(Set(condition)).get.toString) + + result shouldBe expected + } + } + + "contains two conditions of the same type with different keys" in { + val sameTypeConditions = + ( + for { + condition1 ← arbitrary[Condition.NumericCondition] + condition2 ← arbitrary[Condition.NumericCondition] + .suchThat(c ⇒ c.key != condition1.key) + .map(c ⇒ c.copy( + numericComparisonType = condition1.numericComparisonType, + ignoreMissing = condition1.ignoreMissing)) + } yield Set[Condition](condition1, condition2) + ).suchThat(s ⇒ s.size == 2 && s.forall(_.comparisonValues.nonEmpty)) + + forAll(sameTypeConditions) { conditions ⇒ + val c1 :: c2 :: Nil = conditions.toList + def valuesToJson(values: Seq[String]): JsValue = { + values.map(JsString(_)).toList match { + case Nil ⇒ JsNull + case v :: Nil ⇒ v + case vs ⇒ JsArray(vs.toVector) + } + } + + val expected = + JsObject( + c1.comparisonType → JsObject( + c1.key → valuesToJson(c1.comparisonValues), + c2.key → valuesToJson(c2.comparisonValues) + ) + ) + + val result = JsonParser(conditionsToJson(conditions).get.toString) + + result shouldBe expected + } + } + + "merges two conditions with the same type and key" in { + val mergeableConditions = + ( + for { + condition1 ← arbitrary[Condition.StringCondition] + condition2 ← arbitrary[Condition.StringCondition] + .suchThat(c ⇒ c.comparisonValues != condition1.comparisonValues) + .map(c ⇒ c.copy( + stringComparisonType = condition1.stringComparisonType, + ignoreMissing = condition1.ignoreMissing, + key = condition1.key)) + } yield Set[Condition](condition1, condition2) + ) + .suchThat(s ⇒ s.size == 2 && s.forall(_.comparisonValues.nonEmpty)) + + forAll(mergeableConditions) { conditions ⇒ + val c1 :: c2 :: Nil = conditions.toList + + val expected = + JsObject( + c1.comparisonType → JsObject( + c1.key → JsArray((c1.comparisonValues ++ c2.comparisonValues).distinct.map(JsString(_)).toVector) + ) + ) + + val result = JsonParser(conditionsToJson(conditions).get.toString) + + result shouldBe expected + } + } + + "contains two different types of conditions" in { + val twoDifferentConditionTypes = + ( + for { + condition1 ← arbitrary[Condition.ArnCondition] + condition2 ← arbitrary[Condition.DateCondition] + } yield Set[Condition](condition1, condition2) + ).suchThat(s ⇒ s.size == 2) + // Message: {"ArnNotEquals":{"aws:SourceArn":["arn:aws:AwsCodeDeploy:ap-southeast-1:*:*","arn:aws:AwsStorageGateway:us-west-2:158864837822:*","arn:aws:AmazonCognitoSync:ap-northeast-2:190412167942:*","arn:aws:AwsWAF:ap-southeast-2:399393080574:sgz9qc8EiofhW0eqrolgewnql572","arn:aws:AwsKMS:cn-north-1:*:*","arn:aws:AwsSTS:us-west-2:*:*","arn:aws:AwsCloudFormation:us-east-1:*:zhinussm","arn:aws:AutoScaling:cn-north-1:*:*"]},"DateLessThanEqualsIfExists":{"t3vrjs7yhn4midoyawhfjkuxwtvqalsU2zymmjhpwrty0u2MzdwpndadrCruy":["-179800329-10-02T21:39:56.923Z","-175766714-01-18T18:41:36.766Z"]}} + // {"ArnNotEquals":{"aws:SourceArn":["arn:aws:AwsCodeDeploy:ap-southeast-1:*:*","arn:aws:AwsStorageGateway:us-west-2:158864837822:*","arn:aws:AmazonCognitoSync:ap-northeast-2:190412167942:*","arn:aws:AwsWAF:ap-southeast-2:399393080574:sgz9qc8EiofhW0eqrolgewnql572","arn:aws:AwsCodeDeploy:ap-southeast-1:*:*","arn:aws:AwsKMS:cn-north-1:*:*","arn:aws:AwsSTS:us-west-2:*:*","arn:aws:AwsCloudFormation:us-east-1:*:zhinussm","arn:aws:AutoScaling:cn-north-1:*:*"]},"DateLessThanEqualsIfExists":{"t3vrjs7yhn4midoyawhfjkuxwtvqalsU2zymmjhpwrty0u2MzdwpndadrCruy":["-179800329-10-02T21:39:56.923Z","-175766714-01-18T18:41:36.766Z"]}}, + // and {"ArnNotEquals":{"aws:SourceArn":["arn:aws:AwsCodeDeploy:ap-southeast-1:*:*","arn:aws:AwsStorageGateway:us-west-2:158864837822:*","arn:aws:AmazonCognitoSync:ap-northeast-2:190412167942:*","arn:aws:AwsWAF:ap-southeast-2:399393080574:sgz9qc8EiofhW0eqrolgewnql572","arn:aws:AwsKMS:cn-north-1:*:*","arn:aws:AwsSTS:us-west-2:*:*","arn:aws:AwsCloudFormation:us-east-1:*:zhinussm","arn:aws:AutoScaling:cn-north-1:*:*"]},"DateLessThanEqualsIfExists":{"t3vrjs7yhn4midoyawhfjkuxwtvqalsU2zymmjhpwrty0u2MzdwpndadrCruy":["-179800329-10-02T21:39:56.923Z","-175766714-01-18T18:41:36.766Z"]}} + // {"ArnNotEquals":{"aws:SourceArn":["arn:aws:AwsCodeDeploy:ap-southeast-1:*:*","arn:aws:AwsStorageGateway:us-west-2:158864837822:*","arn:aws:AmazonCognitoSync:ap-northeast-2:190412167942:*","arn:aws:AwsWAF:ap-southeast-2:399393080574:sgz9qc8EiofhW0eqrolgewnql572","arn:aws:AwsCodeDeploy:ap-southeast-1:*:*","arn:aws:AwsKMS:cn-north-1:*:*","arn:aws:AwsSTS:us-west-2:*:*","arn:aws:AwsCloudFormation:us-east-1:*:zhinussm","arn:aws:AutoScaling:cn-north-1:*:*"]},"DateLessThanEqualsIfExists":{"t3vrjs7yhn4midoyawhfjkuxwtvqalsU2zymmjhpwrty0u2MzdwpndadrCruy":["-179800329-10-02T21:39:56.923Z","-175766714-01-18T18:41:36.766Z"]}} + + forAll(twoDifferentConditionTypes) { conditions ⇒ + val fwdConditions = conditions.toList + val revConditions = fwdConditions.reverse + def toJson(conditions: List[Condition]): JsObject = { + JsObject( + conditions + .groupBy(_.comparisonType) + .mapValues { c ⇒ + assert(c.size == 1) + val values = c.head.comparisonValues.map(JsString(_)).toList match { + case v :: Nil ⇒ v + case vs ⇒ JsArray(vs.toVector) + } + JsObject(c.head.key → values) + } + ) + } + + val result = JsonParser(conditionsToJson(conditions).get.toString) + + result should (equal (toJson(fwdConditions)) or equal (toJson(revConditions))) + } + } + } + + "a mostly empty Statement" in { + forAll { effect: Statement.Effect ⇒ + val statement = Statement(None, Set.empty, effect, Seq.empty, Seq.empty, Set.empty) + + val expected = JsObject("Effect" → JsString(effect.name)) + val result = JsonParser(statementToJson(statement).toString) + + result shouldBe expected + } + } + + "a mostly empty Policy" in { + forAll { effect: Statement.Effect ⇒ + val policy = Policy(None, None, Seq(Statement(None, Set.empty, effect, Seq.empty, Seq.empty, Set.empty))) + + val expected = + JsObject( + "Statement" → JsArray(JsObject("Effect" → JsString(effect.name))) + ) + + val result = JsonParser(policyToJson(policy).toString) + + result shouldBe expected + } + } + } + + "properly deserialise" - { + "an unknown action" in { + jsonToActions(Some("foo")) shouldBe Seq(Action.NamedAction("foo")) + } + + "a list containing an unknown action" in { + forAll { actions: Seq[Action] ⇒ + whenever(actions != Statement.allActions) { + val json = JsArray((actions.map(a ⇒ JsString(a.name)) :+ JsString("foo")).toVector).toString + val jsonArray = new JSONArray(json) + + jsonToActions(Some(jsonArray)) shouldBe actions :+ Action.NamedAction("foo") + } + } + } + } + + "round-trip" - { + "principal sets" in { + forAll { principals: Set[Principal] ⇒ + jsonToPrincipals(principalsToJson(principals)) shouldBe principals + } + } + + "action lists" in { + forAll { actions: Seq[Action] ⇒ + jsonToActions(actionsToJson(actions)) shouldBe actions + } + } + + "resource lists" in { + forAll { resources: Seq[Resource] ⇒ + jsonToResources(resourcesToJson(resources)) shouldBe resources + } + } + + "condition sets" in { + forAll { conditions: Set[Condition] ⇒ + jsonToConditions(conditionsToJson(conditions)) shouldBe conditions + } + } + + "arbitrary statements" in { + forAll { statement: Statement ⇒ + jsonToStatement(statementToJson(statement)) shouldBe statement + } + } + + "arbitrary policies" in { + forAll { policy: Policy ⇒ + jsonToPolicy(policyToJson(policy)) shouldBe policy + } + } + } + + "be equivalent to the AWS serialisation" - { + val nonBrokenPrincipal = arbitrary[Principal].retryUntil(p ⇒ !p.id.contains("-")) + val nonBrokenPolicy = + for { + // generate a policy with unique condition types + policy ← arbitrary[Policy] + // generate a set of principles that will round-trip + okPrincipals ← Gen.listOfN(policy.statements.size, UtilGen.listOfSqrtN(nonBrokenPrincipal).map(_.toSet)) + // generate a list of IDs for each statement + statementIds ← Gen.listOfN(policy.statements.size, Gen.identifier) + } yield { + // replace all statement principals with round-trippable ones + val okStatements = policy.statements.zip(okPrincipals).zip(statementIds).map { + case ((s, p), i) ⇒ s.copy(principals = p, id = Some(i)) + } + // create a policy that with the OK statements and with the latest policy version + policy.copy(statements = okStatements, version = Some(Policy.Version.`2012-10-17`)) + } + + "when serialising" in { + forAll(nonBrokenPolicy) { policy ⇒ + val json = policyToJson(policy) + val awsPolicy = aws.Policy.fromJson(json) + + awsPolicy.asScala shouldBe policy + } + } + + "when deserialising" in { + forAll(nonBrokenPolicy) { policy ⇒ + val json = policy.asAws.toJson + val fromJson = jsonToPolicy(json) + + fromJson shouldBe policy + } + } + } + } +} diff --git a/aws2scala-core-tests/src/test/scala/com/monsanto/arch/awsutil/auth/policy/PolicySpec.scala b/aws2scala-core-tests/src/test/scala/com/monsanto/arch/awsutil/auth/policy/PolicySpec.scala index 879c4b4..507a437 100644 --- a/aws2scala-core-tests/src/test/scala/com/monsanto/arch/awsutil/auth/policy/PolicySpec.scala +++ b/aws2scala-core-tests/src/test/scala/com/monsanto/arch/awsutil/auth/policy/PolicySpec.scala @@ -1,12 +1,8 @@ package com.monsanto.arch.awsutil.auth.policy -import com.monsanto.arch.awsutil.auth.policy.PolicyDSL._ import com.monsanto.arch.awsutil.converters.CoreConverters._ import com.monsanto.arch.awsutil.test_support.AwsEnumerationBehaviours import com.monsanto.arch.awsutil.testkit.CoreScalaCheckImplicits._ -import com.monsanto.arch.awsutil.testkit.UtilGen -import org.scalacheck.Arbitrary.arbitrary -import org.scalacheck.Gen import org.scalactic.Equality import org.scalatest.FreeSpec import org.scalatest.Matchers._ @@ -24,40 +20,11 @@ class PolicySpec extends FreeSpec with AwsEnumerationBehaviours { } "via JSON" in { - // only get principals that can be process by AWS' fromJson - val nonBrokenPrincipal = arbitrary[Principal].retryUntil(p ⇒ !p.id.contains("-") && p.id != "*") - val nonBrokenPolicy = - for { - // generate a policy with unique condition types - policy ← arbitrary[Policy] - // generate a set of principles that will round-trip - okPrincipals ← Gen.listOfN(policy.statements.size, UtilGen.listOfSqrtN(nonBrokenPrincipal).map(_.toSet)) - } yield { - // replace all statement principals with round-trippable ones - val okStatements = policy.statements.zip(okPrincipals).map { - case (s, p) ⇒ s.copy(principals = p) - } - // create a policy that with the OK statements - policy.copy(statements = okStatements, version = Some(Policy.Version.`2012-10-17`)) - } - forAll(nonBrokenPolicy, maxSize(50)) { policy ⇒ - Policy.fromJson(policy.toJson) should equal (policy) + forAll { policy: Policy ⇒ + Policy.fromJson(policy.toJson) shouldBe policy } } } - - "handle unknown Actions" in { - val result = Policy.fromJson("{\"Statement\": [{\"Effect\":\"Allow\",\"Action\":\"foo\"}]}") - result should equal ( - policy( - statements( - allow( - actions(Action.NamedAction("foo")) - ) - ) - ) - ) - } } "a Policy.Version should" - { @@ -82,13 +49,11 @@ class PolicySpec extends FreeSpec with AwsEnumerationBehaviours { lhs.statements.zip(statements).forall { case (lhsStatement, rhsStatement) ⇒ (rhsStatement.id.isEmpty || lhsStatement.id == rhsStatement.id) && - rhsStatement.principals.diff(lhsStatement.principals).isEmpty && - lhsStatement.principals.diff(rhsStatement.principals).isEmpty && + rhsStatement.principals == lhsStatement.principals && rhsStatement.effect == lhsStatement.effect && rhsStatement.actions == lhsStatement.actions && rhsStatement.resources == lhsStatement.resources && - rhsStatement.conditions.diff(lhsStatement.conditions).isEmpty && - lhsStatement.conditions.diff(rhsStatement.conditions).isEmpty + rhsStatement.conditions == lhsStatement.conditions } case _ ⇒ false } diff --git a/aws2scala-core/src/main/scala/com/monsanto/arch/awsutil/auth/policy/Policy.scala b/aws2scala-core/src/main/scala/com/monsanto/arch/awsutil/auth/policy/Policy.scala index 6c529b4..771c8a5 100644 --- a/aws2scala-core/src/main/scala/com/monsanto/arch/awsutil/auth/policy/Policy.scala +++ b/aws2scala-core/src/main/scala/com/monsanto/arch/awsutil/auth/policy/Policy.scala @@ -1,16 +1,13 @@ package com.monsanto.arch.awsutil.auth.policy -import com.amazonaws.auth.{policy ⇒ aws} -import com.monsanto.arch.awsutil.converters.CoreConverters._ - final case class Policy(version: Option[Policy.Version], id: Option[String], statements: Seq[Statement]) { - def toJson: String = this.asAws.toJson + def toJson: String = PolicyJsonSupport.policyToJson(this) } object Policy { - def fromJson(json: String): Policy = aws.Policy.fromJson(json).asScala + def fromJson(json: String): Policy = PolicyJsonSupport.jsonToPolicy(json) /** Type for all policy versions. */ sealed abstract class Version(val id: String) diff --git a/aws2scala-core/src/main/scala/com/monsanto/arch/awsutil/auth/policy/PolicyJsonSupport.scala b/aws2scala-core/src/main/scala/com/monsanto/arch/awsutil/auth/policy/PolicyJsonSupport.scala new file mode 100644 index 0000000..aa971f8 --- /dev/null +++ b/aws2scala-core/src/main/scala/com/monsanto/arch/awsutil/auth/policy/PolicyJsonSupport.scala @@ -0,0 +1,192 @@ +package com.monsanto.arch.awsutil.auth.policy + +import com.amazonaws.util.json.{JSONArray, JSONObject} + +import scala.collection.JavaConverters._ + +private[awsutil] object PolicyJsonSupport { + def policyToJson(policy: Policy): String = { + val jsonObject = new JSONObject() + jsonObject.putOpt("Version", policy.version.map(_.id).orNull) + jsonObject.putOpt("Id", policy.id.orNull) + jsonObject.put("Statement", policy.statements.map(statementToJson).asJavaCollection) + jsonObject.toString + } + + def jsonToPolicy(jsonString: String): Policy = { + val json = new JSONObject(jsonString) + val version = Option(json.optString("Version", null)).map(Policy.Version.apply) + val id = Option(json.optString("Id", null)) + val statements = json.getJSONArray("Statement").asScala[JSONObject].map(jsonToStatement).toList + Policy(version, id, statements) + } + + def statementToJson(statement: Statement): JSONObject = { + val jsonObject = new JSONObject + jsonObject.putOpt("Sid", statement.id.orNull) + jsonObject.putOpt("Principal", principalsToJson(statement.principals).orNull) + jsonObject.put("Effect", statement.effect.name) + jsonObject.putOpt("Action", actionsToJson(statement.actions).orNull) + jsonObject.putOpt("Resource", resourcesToJson(statement.resources).orNull) + jsonObject.putOpt("Condition", conditionsToJson(statement.conditions).orNull) + jsonObject + } + + def jsonToStatement(jsonObject: JSONObject): Statement = { + val sid = Option(jsonObject.optString("Sid", null)) + val principals = jsonToPrincipals(Option(jsonObject.opt("Principal"))) + val effect = Statement.Effect(jsonObject.getString("Effect")) + val actions = jsonToActions(Option(jsonObject.opt("Action"))) + val resources = jsonToResources(Option(jsonObject.opt("Resource"))) + val conditions = jsonToConditions(Option(jsonObject.opt("Condition").asInstanceOf[JSONObject])) + Statement(sid, principals, effect, actions, resources, conditions) + } + + def principalsToJson(principals: Set[Principal]): Option[AnyRef] = { + if (principals.isEmpty) { + None + } else if (principals == Statement.allPrincipals) { + Some("*") + } else { + val jsonObject = new JSONObject() + principals.groupBy(_.provider).foreach { grouped ⇒ + val provider = grouped._1 + val ids = grouped._2.map(_.id).toList + ids match { + case id :: Nil ⇒ + jsonObject.put(provider, id) + case _ ⇒ + jsonObject.put(provider, new JSONArray(ids.asJavaCollection)) + } + } + Some(jsonObject) + } + } + + def jsonToPrincipals(jsonObject: Option[AnyRef]): Set[Principal] = { + jsonObject match { + case None ⇒ + Set.empty + case Some("*") ⇒ + Statement.allPrincipals + case Some(jo: JSONObject) ⇒ + jo.asScala.flatMap { entry ⇒ + val (provider, value) = entry + value match { + case id: String ⇒ + Seq(Principal(provider, id)) + case ids: JSONArray ⇒ + ids.asScala[String].map(id ⇒ Principal(provider, id)) + } + }.toSet + case _ ⇒ throw new IllegalArgumentException(s"$jsonObject is not a valid principals JSON value.") + } + } + + def actionsToJson(actions: Seq[Action]): Option[AnyRef] = + actions.toList match { + case Nil ⇒ None + case action :: Nil ⇒ Some(action.name) + case _ ⇒ Some(new JSONArray(actions.map(_.name).asJavaCollection)) + } + + def jsonToActions(json: Option[AnyRef]): Seq[Action] = { + def getAction(name: String): Action = + name match { + case Action.fromName(action) ⇒ action + case _ ⇒ Action.NamedAction(name) + } + + json match { + case None ⇒ Seq.empty + case Some(name: String) ⇒ Seq(getAction(name)) + case Some(names: JSONArray) ⇒ names.asScala[String].map(getAction) + case Some(x) ⇒ throw new IllegalArgumentException(s"$x is not a valid actions JSON value.") + } + } + + def resourcesToJson(resources: Seq[Resource]): Option[AnyRef] = + resources.toList match { + case Nil ⇒ None + case resource :: Nil ⇒ Some(resource.id) + case _ ⇒ Some(new JSONArray(resources.map(_.id).asJavaCollection)) + } + + def jsonToResources(json: Option[AnyRef]): Seq[Resource] = + json match { + case None ⇒ Seq.empty + case Some(id: String) ⇒ Seq(Resource(id)) + case Some(jsArray: JSONArray) ⇒ jsArray.asScala[String].map(Resource(_)) + case Some(x) ⇒ throw new IllegalArgumentException(s"$x is not a valid resources JSON value.") + } + + def conditionsToJson(conditions: Set[Condition]): Option[JSONObject] = { + if (conditions.isEmpty) { + None + } else { + val comparisonValuesByTypeAndKey = + conditions + .groupBy(_.comparisonType) + .mapValues { byTypeConditions ⇒ + byTypeConditions + .groupBy(_.key) + .mapValues { byTypeAndKeyConditions ⇒ + byTypeAndKeyConditions.toList.flatMap(_.comparisonValues).distinct + } + } + val result = new JSONObject() + comparisonValuesByTypeAndKey.foreach { typeEntry ⇒ + val (comparisonType, byTypeEntry) = typeEntry + val typeResult = new JSONObject() + byTypeEntry.foreach { keyEntry ⇒ + val (key, values) = keyEntry + values match { + case Nil ⇒ + // maybe do something? + case value :: Nil ⇒ + typeResult.put(key, value) + case _ ⇒ + val jsArray = new JSONArray() + values.foreach(v ⇒ jsArray.put(v)) + typeResult.put(key, jsArray) + } + } + result.put(comparisonType, typeResult) + } + Some(result) + } + } + + def jsonToConditions(json: Option[JSONObject]): Set[Condition] = + json match { + case None ⇒ Set.empty + case Some(jsonObject) ⇒ + jsonObject.asScala.flatMap { typeMapping ⇒ + val (comparisonType, keysAndValues: JSONObject) = typeMapping + keysAndValues.asScala.map { keyAndValues ⇒ + val (key, jsonValues) = keyAndValues + jsonValues match { + case value: String ⇒ + Condition(key, comparisonType, Seq(value)) + case values: JSONArray ⇒ + Condition(key, comparisonType, values.asScala[String].toList) + } + } + }.toSet + } + + private implicit class JsonArrayConverter(val array: JSONArray) extends AnyVal { + def asScala[T]: Seq[T] = for (i ← 0.until(array.length())) yield array.get(i).asInstanceOf[T] + } + + private implicit class JsonObjectConverter(val jsObject: JSONObject) extends AnyVal { + def asScala: Seq[(String,AnyRef)] = { + val names = jsObject.names() + for (i ← 0.until(jsObject.length())) yield { + val key = names.get(i).toString + val value = jsObject.get(key) + (key, value) + } + } + } +} diff --git a/build.sbt b/build.sbt index 71ce7a3..7d31cac 100644 --- a/build.sbt +++ b/build.sbt @@ -227,7 +227,7 @@ lazy val iamTestkit = Project("aws2scala-iam-testkit", file("aws2scala-iam-testk commonSettings, bintrayPublishingSettings, description := "Test utility library for aws2scala-iam", - libraryDependencies += scalaCheck + libraryDependencies ++= Seq(scalaCheck, sprayJson) ) lazy val iamTests = Project("aws2scala-iam-tests", file("aws2scala-iam-tests"))