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))
+ }
+
+}