You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by ra...@apache.org on 2018/09/07 16:37:28 UTC

[incubator-openwhisk] branch master updated: Restrict allowed namespaces when creating action of certain kinds (#3661)

This is an automated email from the ASF dual-hosted git repository.

rabbah pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-openwhisk.git


The following commit(s) were added to refs/heads/master by this push:
     new 09c5305  Restrict allowed namespaces when creating action of certain kinds (#3661)
09c5305 is described below

commit 09c53055b28c723d8d0a4085b1f0293649a0aabf
Author: Andy Steed <an...@gmail.com>
AuthorDate: Fri Sep 7 09:37:25 2018 -0700

    Restrict allowed namespaces when creating action of certain kinds (#3661)
---
 .../src/main/scala/whisk/core/WhiskConfig.scala    |  3 +-
 .../main/scala/whisk/core/entity/Identity.scala    |  5 +-
 .../src/main/scala/whisk/http/ErrorResponse.scala  |  2 +
 .../main/scala/whisk/core/controller/Actions.scala |  5 +-
 .../scala/whisk/core/entitlement/Entitlement.scala | 73 ++++++++++++++-----
 .../whisk/core/entitlement/KindRestrictor.scala    | 59 +++++++++++++++
 .../controller/test/EntitlementProviderTests.scala | 36 ++++++++-
 .../core/controller/test/KindRestrictorTests.scala | 85 ++++++++++++++++++++++
 8 files changed, 241 insertions(+), 27 deletions(-)

diff --git a/common/scala/src/main/scala/whisk/core/WhiskConfig.scala b/common/scala/src/main/scala/whisk/core/WhiskConfig.scala
index df24317..d4e677f 100644
--- a/common/scala/src/main/scala/whisk/core/WhiskConfig.scala
+++ b/common/scala/src/main/scala/whisk/core/WhiskConfig.scala
@@ -206,12 +206,13 @@ object ConfigKeys {
   val userEvents = "whisk.user-events"
 
   val runtimes = "whisk.runtimes"
+  val runtimesWhitelists = s"$runtimes.whitelists"
 
   val db = "whisk.db"
 
   val docker = "whisk.docker"
   val dockerClient = s"$docker.client"
-  val dockerContainerFactory = s"${docker}.container-factory"
+  val dockerContainerFactory = s"$docker.container-factory"
   val runc = "whisk.runc"
   val runcTimeouts = s"$runc.timeouts"
 
diff --git a/common/scala/src/main/scala/whisk/core/entity/Identity.scala b/common/scala/src/main/scala/whisk/core/entity/Identity.scala
index f0ebc21..0f97d5e 100644
--- a/common/scala/src/main/scala/whisk/core/entity/Identity.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/Identity.scala
@@ -31,10 +31,11 @@ import whisk.core.entitlement.Privilege
 
 case class UserLimits(invocationsPerMinute: Option[Int] = None,
                       concurrentInvocations: Option[Int] = None,
-                      firesPerMinute: Option[Int] = None)
+                      firesPerMinute: Option[Int] = None,
+                      allowedKinds: Option[Set[String]] = None)
 
 object UserLimits extends DefaultJsonProtocol {
-  implicit val serdes = jsonFormat3(UserLimits.apply)
+  implicit val serdes = jsonFormat4(UserLimits.apply)
 }
 
 protected[core] case class Namespace(name: EntityName, uuid: UUID)
diff --git a/common/scala/src/main/scala/whisk/http/ErrorResponse.scala b/common/scala/src/main/scala/whisk/http/ErrorResponse.scala
index 5a31855..010979e 100644
--- a/common/scala/src/main/scala/whisk/http/ErrorResponse.scala
+++ b/common/scala/src/main/scala/whisk/http/ErrorResponse.scala
@@ -81,6 +81,8 @@ object Messages {
   val notAuthorizedtoOperateOnResource = "The supplied authentication is not authorized to access this resource."
   def notAuthorizedtoAccessResource(value: String) =
     s"The supplied authentication is not authorized to access '$value'."
+  def notAuthorizedtoActionKind(value: String) =
+    s"The supplied authentication is not authorized to access actions of kind '$value'."
 
   /** Standard error message for malformed fully qualified entity names. */
   val malformedFullyQualifiedEntityName =
diff --git a/core/controller/src/main/scala/whisk/core/controller/Actions.scala b/core/controller/src/main/scala/whisk/core/controller/Actions.scala
index bd92c58..8828241 100644
--- a/core/controller/src/main/scala/whisk/core/controller/Actions.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/Actions.scala
@@ -187,8 +187,11 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
     parameter('overwrite ? false) { overwrite =>
       entity(as[WhiskActionPut]) { content =>
         val request = content.resolve(user.namespace)
+        val checkAdditionalPrivileges = entitleReferencedEntities(user, Privilege.READ, request.exec).flatMap {
+          case _ => entitlementProvider.check(user, content.exec)
+        }
 
-        onComplete(entitleReferencedEntities(user, Privilege.READ, request.exec)) {
+        onComplete(checkAdditionalPrivileges) {
           case Success(_) =>
             putEntity(WhiskAction, entityStore, entityName.toDocId, overwrite, update(user, request) _, () => {
               make(user, entityName, request)
diff --git a/core/controller/src/main/scala/whisk/core/entitlement/Entitlement.scala b/core/controller/src/main/scala/whisk/core/entitlement/Entitlement.scala
index db09d87..1f3f53c 100644
--- a/core/controller/src/main/scala/whisk/core/entitlement/Entitlement.scala
+++ b/core/controller/src/main/scala/whisk/core/entitlement/Entitlement.scala
@@ -47,18 +47,21 @@ package object types {
  * Resource is a type that encapsulates details relevant to identify a specific resource.
  * It may be an entire collection, or an element in a collection.
  *
- * @param namespace the namespace the resource resides in
+ * @param namespace  the namespace the resource resides in
  * @param collection the collection (e.g., actions, triggers) identifying a resource
- * @param entity an optional entity name that identifies a specific item in the collection
- * @param env an optional environment to bind to the resource during an activation
+ * @param entity     an optional entity name that identifies a specific item in the collection
+ * @param env        an optional environment to bind to the resource during an activation
  */
 protected[core] case class Resource(namespace: EntityPath,
                                     collection: Collection,
                                     entity: Option[String],
                                     env: Option[Parameters] = None) {
   def parent: String = collection.path + EntityPath.PATHSEP + namespace
+
   def id: String = parent + entity.map(EntityPath.PATHSEP + _).getOrElse("")
+
   def fqname: String = namespace.asString + entity.map(EntityPath.PATHSEP + _).getOrElse("")
+
   override def toString: String = id
 }
 
@@ -92,13 +95,14 @@ protected[core] abstract class EntitlementProvider(
    * controllers
    */
   private def overcommit(clusterSize: Int) = if (clusterSize > 1) 1.2 else 1
+
   private def dilateLimit(limit: Int): Int = Math.ceil(limit.toDouble * overcommit(loadBalancer.clusterSize)).toInt
 
   /**
    * Calculates a possibly dilated limit relative to the current user.
    *
    * @param defaultLimit the default limit across the whole system
-   * @param user the user to apply that limit to
+   * @param user         the user to apply that limit to
    * @return a calculated limit
    */
   private def calculateLimit(defaultLimit: Int, overrideLimit: Identity => Option[Int])(user: Identity): Int = {
@@ -113,7 +117,7 @@ protected[core] abstract class EntitlementProvider(
    * limit, so it needs to be divided between the parties who want to perform that check.
    *
    * @param defaultLimit the default limit across the whole system
-   * @param user the user to apply that limit to
+   * @param user         the user to apply that limit to
    * @return a calculated limit
    */
   private def calculateIndividualLimit(defaultLimit: Int, overrideLimit: Identity => Option[Int])(
@@ -154,8 +158,8 @@ protected[core] abstract class EntitlementProvider(
   /**
    * Grants a subject the right to access a resources.
    *
-   * @param subject the subject to grant right to
-   * @param right the privilege to grant the subject
+   * @param user     the subject to grant right to
+   * @param right    the privilege to grant the subject
    * @param resource the resource to grant the subject access to
    * @return a promise that completes with true iff the subject is granted the right to access the requested resource
    */
@@ -165,8 +169,8 @@ protected[core] abstract class EntitlementProvider(
   /**
    * Revokes a subject the right to access a resources.
    *
-   * @param subject the subject to revoke right to
-   * @param right the privilege to revoke the subject
+   * @param user     the subject to revoke right to
+   * @param right    the privilege to revoke the subject
    * @param resource the resource to revoke the subject access to
    * @return a promise that completes with true iff the subject is revoked the right to access the requested resource
    */
@@ -176,8 +180,8 @@ protected[core] abstract class EntitlementProvider(
   /**
    * Checks if a subject is entitled to a resource because it was granted the right explicitly.
    *
-   * @param subject the subject to check rights for
-   * @param right the privilege the subject is requesting
+   * @param user     the subject to check rights for
+   * @param right    the privilege the subject is requesting
    * @param resource the resource the subject requests access to
    * @return a promise that completes with true iff the subject is permitted to access the request resource
    */
@@ -197,6 +201,35 @@ protected[core] abstract class EntitlementProvider(
       .flatMap(_ => checkThrottleOverload(concurrentInvokeThrottler.check(user), user))
   }
 
+  private val kindRestrictor = {
+    import pureconfig.loadConfigOrThrow
+    import whisk.core.ConfigKeys
+    case class AllowedKinds(whitelist: Option[Set[String]] = None)
+    val allowedKinds = loadConfigOrThrow[AllowedKinds](ConfigKeys.runtimes)
+    KindRestrictor(allowedKinds.whitelist)
+  }
+
+  /**
+   * Checks if an action kind is allowed for a given subject.
+   *
+   * @param user the identity to check for restrictions
+   * @param exec the action executable details
+   * @return a promise that completes with success iff the user's action kind is allowed
+   */
+  protected[core] def check(user: Identity, exec: Option[Exec])(implicit transid: TransactionId): Future[Unit] = {
+    exec
+      .map {
+        case e =>
+          if (kindRestrictor.check(user, e.kind)) {
+            Future.successful(())
+          } else {
+            Future.failed(
+              RejectRequest(Forbidden, Some(ErrorResponse(Messages.notAuthorizedtoActionKind(e.kind), transid))))
+          }
+      }
+      .getOrElse(Future.successful(()))
+  }
+
   /**
    * Checks if a subject has the right to access a specific resource. The entitlement may be implicit,
    * that is, inferred based on namespaces that a subject belongs to and the namespace of the
@@ -208,8 +241,8 @@ protected[core] abstract class EntitlementProvider(
    * implicitly or explicitly granted. Instead, resolve the package binding first and use the alternate
    * method which authorizes a set of resources.
    *
-   * @param user the subject to check rights for
-   * @param right the privilege the subject is requesting (applies to the entire set of resources)
+   * @param user     the subject to check rights for
+   * @param right    the privilege the subject is requesting (applies to the entire set of resources)
    * @param resource the resource the subject requests access to
    * @return a promise that completes with success iff the subject is permitted to access the requested resource
    */
@@ -237,9 +270,9 @@ protected[core] abstract class EntitlementProvider(
    * resource for example, or explicit. The implicit check is computed here. The explicit check
    * is delegated to the service implementing this interface.
    *
-   * @param user the subject identity to check rights for
-   * @param right the privilege the subject is requesting (applies to the entire set of resources)
-   * @param resources the set of resources the subject requests access to
+   * @param user       the subject identity to check rights for
+   * @param right      the privilege the subject is requesting (applies to the entire set of resources)
+   * @param resources  the set of resources the subject requests access to
    * @param noThrottle ignore throttle limits
    * @return a promise that completes with success iff the subject is permitted to access all of the requested resources
    */
@@ -312,8 +345,8 @@ protected[core] abstract class EntitlementProvider(
    * While it is possible for the set of resources to contain more than one action or trigger, the plurality is ignored and treated
    * as one activation since these should originate from a single macro resources (e.g., a sequence).
    *
-   * @param user the subject identity to check rights for
-   * @param right the privilege, if ACTIVATE then check quota else return None
+   * @param user      the subject identity to check rights for
+   * @param right     the privilege, if ACTIVATE then check quota else return None
    * @param resources the set of resources must contain at least one resource that can be activated else return None
    * @return future completing successfully if user is below limits else failing with a rejection
    */
@@ -334,8 +367,8 @@ protected[core] abstract class EntitlementProvider(
    * While it is possible for the set of resources to contain more than one action, the plurality is ignored and treated
    * as one activation since these should originate from a single macro resources (e.g., a sequence).
    *
-   * @param user the subject identity to check rights for
-   * @param right the privilege, if ACTIVATE then check quota else return None
+   * @param user      the subject identity to check rights for
+   * @param right     the privilege, if ACTIVATE then check quota else return None
    * @param resources the set of resources must contain at least one resource that can be activated else return None
    * @return future completing successfully if user is below limits else failing with a rejection
    */
diff --git a/core/controller/src/main/scala/whisk/core/entitlement/KindRestrictor.scala b/core/controller/src/main/scala/whisk/core/entitlement/KindRestrictor.scala
new file mode 100644
index 0000000..a569d05
--- /dev/null
+++ b/core/controller/src/main/scala/whisk/core/entitlement/KindRestrictor.scala
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package whisk.core.entitlement
+
+import whisk.common.{Logging, TransactionId}
+import whisk.core.entity.Identity
+
+/**
+ * The runtimes manifest specifies all runtimes enabled for the deployment.
+ * Not all runtimes are available for all subject however.
+ *
+ * A subject is entitled to a runtime (kind) if:
+ * 1. they are explicitly granted rights to it in their identity record, or
+ * 2. the runtime kind is whitelisted
+ *
+ * If a white list is not specified (i.e., whitelist == None), then all runtimes are allowed.
+ * In other words, no whitelist is the same as setting the white list to all allowed runtimes.
+ *
+ * @param whitelist set of default allowed kinds when not explicitly available via namespace limits.
+ */
+case class KindRestrictor(whitelist: Option[Set[String]] = None)(implicit logging: Logging) {
+
+  logging.info(
+    this, {
+      whitelist
+        .map {
+          case list if list.nonEmpty => s"white-listed kinds: ${list.mkString(", ")}"
+          case _                     => "no kinds are allowed, the white-list is empty"
+        }
+        .getOrElse("all kinds are allowed, the white-list is not specified")
+    })(TransactionId.controller)
+
+  def check(user: Identity, kind: String): Boolean = {
+    user.limits.allowedKinds
+      .orElse(whitelist)
+      .map(allowed => allowed.contains(kind))
+      .getOrElse(true)
+  }
+
+}
+
+object KindRestrictor {
+  def apply(whitelist: Set[String])(implicit logging: Logging) = new KindRestrictor(Some(whitelist))
+}
diff --git a/tests/src/test/scala/whisk/core/controller/test/EntitlementProviderTests.scala b/tests/src/test/scala/whisk/core/controller/test/EntitlementProviderTests.scala
index cd163df..2a3042d 100644
--- a/tests/src/test/scala/whisk/core/controller/test/EntitlementProviderTests.scala
+++ b/tests/src/test/scala/whisk/core/controller/test/EntitlementProviderTests.scala
@@ -20,17 +20,15 @@ package whisk.core.controller.test
 import scala.collection.mutable.ListBuffer
 import scala.concurrent.Await
 import scala.concurrent.duration.DurationInt
-
 import org.junit.runner.RunWith
 import org.scalatest.concurrent.ScalaFutures
 import org.scalatest.junit.JUnitRunner
-
 import akka.http.scaladsl.model.StatusCodes._
-
 import whisk.core.controller.RejectRequest
 import whisk.core.entitlement._
 import whisk.core.entitlement.Privilege._
 import whisk.core.entity._
+import whisk.core.entity.ExecManifest.{ImageName, RuntimeManifest}
 import whisk.http.Messages
 
 /**
@@ -55,6 +53,13 @@ class EntitlementProviderTests extends ControllerTestCommon with ScalaFutures {
   val adminUser = WhiskAuthHelpers.newIdentity(Subject("admin"))
   val guestUser = WhiskAuthHelpers.newIdentity(Subject("anonym"))
 
+  val allowedKinds = Set("nodejs:6", "python")
+  val disallowedKinds = Set("golang", "blackbox")
+
+  def getExec(kind: String): Exec = {
+    CodeExecAsString(RuntimeManifest(kind, ImageName(kind ++ "action")), "function main(){}", None)
+  }
+
   it should "authorize a user to only read from their collection" in {
     implicit val tid = transid()
     val collections = Seq(ACTIONS, RULES, TRIGGERS, PACKAGES, ACTIVATIONS, NAMESPACES)
@@ -681,4 +686,29 @@ class EntitlementProviderTests extends ControllerTestCommon with ScalaFutures {
         Await.ready(check, requestTimeout).eitherValue.get shouldBe expected
     }
   }
+
+  it should "restrict access to disallowed action kinds for a subject" in {
+    implicit val tid = transid()
+    implicit val ep = entitlementProvider
+    val subject = WhiskAuthHelpers.newIdentity().copy(limits = UserLimits(allowedKinds = Some(allowedKinds)))
+
+    disallowedKinds.foreach(k => {
+      val ex = intercept[RejectRequest] {
+        Await.result(ep.check(subject, Some(getExec(k))), 1.seconds)
+      }
+
+      ex.code shouldBe Forbidden
+      ex.message.get.error shouldBe Messages.notAuthorizedtoActionKind(k)
+    })
+  }
+
+  it should "allow access to whitelisted action kinds for a subject" in {
+    implicit val tid = transid()
+    implicit val ep = entitlementProvider
+    val subject = WhiskAuthHelpers.newIdentity().copy(limits = UserLimits(allowedKinds = Some(allowedKinds)))
+
+    Await.result(ep.check(subject, None), 1.seconds)
+    allowedKinds.foreach(k => Await.result(ep.check(subject, Some(getExec(k))), 1.seconds))
+  }
+
 }
diff --git a/tests/src/test/scala/whisk/core/controller/test/KindRestrictorTests.scala b/tests/src/test/scala/whisk/core/controller/test/KindRestrictorTests.scala
new file mode 100644
index 0000000..b5ffc34
--- /dev/null
+++ b/tests/src/test/scala/whisk/core/controller/test/KindRestrictorTests.scala
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package whisk.core.controller.test
+
+import common.StreamLogging
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.{FlatSpec, Matchers}
+import whisk.core.entitlement.KindRestrictor
+import whisk.core.entity._
+
+/**
+ * Tests authorization handler which guards resources.
+ *
+ * Unit tests of the controller service as a standalone component.
+ * These tests exercise a fresh instance of the service object in memory -- these
+ * tests do NOT communication with a whisk deployment.
+ *
+ * @Idioglossia
+ * "using Specification DSL to write unit tests, as in should, must, not, be"
+ */
+@RunWith(classOf[JUnitRunner])
+class KindRestrictorTests extends FlatSpec with Matchers with StreamLogging {
+
+  behavior of "Kind Restrictor"
+
+  val allowedKinds = Set("nodejs:6", "python")
+  val disallowedKinds = Set("golang", "blackbox")
+  val allKinds = allowedKinds ++ disallowedKinds
+
+  it should "grant subject access to all kinds when no limits exist and no white list is defined" in {
+    val subject = WhiskAuthHelpers.newIdentity()
+    val kr = KindRestrictor()
+    allKinds.foreach(k => kr.check(subject, k) shouldBe true)
+  }
+
+  it should "not grant subject access to any kinds if limit is the empty set" in {
+    val subject = WhiskAuthHelpers.newIdentity().copy(limits = UserLimits(allowedKinds = Some(Set.empty)))
+    val kr = KindRestrictor()
+    allKinds.foreach(k => kr.check(subject, k) shouldBe false)
+  }
+
+  it should "not grant subject access to any kinds if white list is the empty set" in {
+    val subject = WhiskAuthHelpers.newIdentity()
+    val kr = KindRestrictor(Set[String]())
+    allKinds.foreach(k => kr.check(subject, k) shouldBe false)
+  }
+
+  it should "grant subject access only to subject-limited kinds" in {
+    val subject = WhiskAuthHelpers.newIdentity().copy(limits = UserLimits(allowedKinds = Some(allowedKinds)))
+    val kr = KindRestrictor()
+    allowedKinds.foreach(k => kr.check(subject, k) shouldBe true)
+    disallowedKinds.foreach(k => kr.check(subject, k) shouldBe false)
+  }
+
+  it should "grant subject access to white listed kinds when no limits exist" in {
+    val subject = WhiskAuthHelpers.newIdentity()
+    val kr = KindRestrictor(allowedKinds)
+    allowedKinds.foreach(k => kr.check(subject, k) shouldBe true)
+    disallowedKinds.foreach(k => kr.check(subject, k) shouldBe false)
+  }
+
+  it should "grant subject access only to explicitly limited kind" in {
+    val explicitKind = allowedKinds.head
+    val subject = WhiskAuthHelpers.newIdentity().copy(limits = UserLimits(allowedKinds = Some(Set(explicitKind))))
+    val kr = KindRestrictor(allowedKinds.tail)
+    allKinds.foreach(k => kr.check(subject, k) shouldBe (k == explicitKind))
+  }
+
+}