Skip to content

Commit e255126

Browse files
authored
Execute Only for Shared Actions (#4935)
* Initial Commit of Execute Only
1 parent 433376a commit e255126

File tree

7 files changed

+180
-23
lines changed

7 files changed

+180
-23
lines changed

common/scala/src/main/resources/application.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ kamon {
9999
}
100100

101101
whisk {
102+
shared-packages-execute-only = false
102103
metrics {
103104
# Enable/disable Prometheus support. If enabled then metrics would be exposed at `/metrics` endpoint
104105
# If Prometheus is enabled then please review `kamon.metric.tick-interval` (set to 1 sec by default above).

common/scala/src/main/scala/org/apache/openwhisk/core/WhiskConfig.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ object ConfigKeys {
264264
val featureFlags = "whisk.feature-flags"
265265

266266
val whiskConfig = "whisk.config"
267+
val sharedPackageExecuteOnly = s"whisk.shared-packages-execute-only"
267268
val swaggerUi = "whisk.swagger-ui"
268269

269270
val disableStoreResult = s"$activation.disable-store-result"

common/scala/src/main/scala/org/apache/openwhisk/http/ErrorResponse.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,15 @@ object Messages {
229229

230230
/** Indicates that the container for the action could not be started. */
231231
val resourceProvisionError = "Failed to provision resources to run the action."
232+
233+
def forbiddenGetActionBinding(entityDocId: String) =
234+
s"GET not permitted for '$entityDocId'. Resource does not exist or is an action in a shared package binding."
235+
def forbiddenGetAction(entityPath: String) =
236+
s"GET not permitted for '$entityPath' since it's an action in a shared package"
237+
def forbiddenGetPackageBinding(packageName: String) =
238+
s"GET not permitted since $packageName is a binding of a shared package"
239+
def forbiddenGetPackage(packageName: String) =
240+
s"GET not permitted for '$packageName' since it's a shared package"
232241
}
233242

234243
/** Replaces rejections with Json object containing cause and transaction id. */

core/controller/src/main/scala/org/apache/openwhisk/core/controller/Actions.scala

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import org.apache.openwhisk.http.Messages._
4343
import org.apache.openwhisk.core.entitlement.Resource
4444
import org.apache.openwhisk.core.entitlement.Collection
4545
import org.apache.openwhisk.core.loadBalancer.LoadBalancerException
46+
import pureconfig._
47+
import org.apache.openwhisk.core.ConfigKeys
4648

4749
/**
4850
* A singleton object which defines the properties that must be present in a configuration
@@ -102,6 +104,10 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
102104
/** Database service to get activations. */
103105
protected val activationStore: ActivationStore
104106

107+
/** Config flag for Execute Only for Actions in Shared Packages */
108+
protected def executeOnly =
109+
loadConfigOrThrow[Boolean](ConfigKeys.sharedPackageExecuteOnly)
110+
105111
/** Entity normalizer to JSON object. */
106112
import RestApiCommons.emptyEntityToJsObject
107113

@@ -330,6 +336,50 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
330336
deleteEntity(WhiskAction, entityStore, entityName.toDocId, (a: WhiskAction) => Future.successful({}))
331337
}
332338

339+
/** Checks for package binding case. we don't want to allow get for a package binding in shared package */
340+
private def fetchEntity(entityName: FullyQualifiedEntityName, env: Option[Parameters], code: Boolean)(
341+
implicit transid: TransactionId) = {
342+
val resolvedPkg: Future[Either[String, FullyQualifiedEntityName]] = if (entityName.path.defaultPackage) {
343+
Future.successful(Right(entityName))
344+
} else {
345+
WhiskPackage.resolveBinding(entityStore, entityName.path.toDocId, mergeParameters = true).map { pkg =>
346+
val originalPackageLocation = pkg.fullyQualifiedName(withVersion = false).namespace
347+
if (executeOnly && originalPackageLocation != entityName.namespace) {
348+
Left(forbiddenGetActionBinding(entityName.toDocId.asString))
349+
} else {
350+
Right(entityName)
351+
}
352+
}
353+
}
354+
onComplete(resolvedPkg) {
355+
case Success(pkgFuture) =>
356+
pkgFuture match {
357+
case Left(f) => terminate(Forbidden, f)
358+
case Right(_) =>
359+
if (code) {
360+
getEntity(WhiskAction.resolveActionAndMergeParameters(entityStore, entityName), Some {
361+
action: WhiskAction =>
362+
val mergedAction = env map {
363+
action inherit _
364+
} getOrElse action
365+
complete(OK, mergedAction)
366+
})
367+
} else {
368+
getEntity(WhiskActionMetaData.resolveActionAndMergeParameters(entityStore, entityName), Some {
369+
action: WhiskActionMetaData =>
370+
val mergedAction = env map {
371+
action inherit _
372+
} getOrElse action
373+
complete(OK, mergedAction)
374+
})
375+
}
376+
}
377+
case Failure(t: Throwable) =>
378+
logging.error(this, s"[GET] package ${entityName.path.toDocId} failed: ${t.getMessage}")
379+
terminate(InternalServerError)
380+
}
381+
}
382+
333383
/**
334384
* Gets action. The action name is prefixed with the namespace to create the primary index key.
335385
*
@@ -341,22 +391,12 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
341391
override def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(
342392
implicit transid: TransactionId) = {
343393
parameter('code ? true) { code =>
344-
code match {
345-
case true =>
346-
getEntity(WhiskAction.resolveActionAndMergeParameters(entityStore, entityName), Some { action: WhiskAction =>
347-
val mergedAction = env map {
348-
action inherit _
349-
} getOrElse action
350-
complete(OK, mergedAction)
351-
})
352-
case false =>
353-
getEntity(WhiskActionMetaData.resolveActionAndMergeParameters(entityStore, entityName), Some {
354-
action: WhiskActionMetaData =>
355-
val mergedAction = env map {
356-
action inherit _
357-
} getOrElse action
358-
complete(OK, mergedAction)
359-
})
394+
//check if execute only is enabled, and if there is a discrepancy between the current user's namespace
395+
//and that of the entity we are trying to fetch
396+
if (executeOnly && user.namespace.name != entityName.namespace) {
397+
terminate(Forbidden, forbiddenGetAction(entityName.path.asString))
398+
} else {
399+
fetchEntity(entityName, env, code)
360400
}
361401
}
362402
}

core/controller/src/main/scala/org/apache/openwhisk/core/controller/Packages.scala

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,10 @@ package org.apache.openwhisk.core.controller
1919

2020
import scala.concurrent.Future
2121
import scala.util.{Failure, Success}
22-
2322
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
2423
import akka.http.scaladsl.model.StatusCodes._
2524
import akka.http.scaladsl.server.{RequestContext, RouteResult}
2625
import akka.http.scaladsl.unmarshalling.Unmarshaller
27-
2826
import org.apache.openwhisk.common.TransactionId
2927
import org.apache.openwhisk.core.controller.RestApiCommons.{ListLimit, ListSkip}
3028
import org.apache.openwhisk.core.database.{CacheChangeNotification, DocumentTypeMismatchException, NoDocumentException}
@@ -33,6 +31,9 @@ import org.apache.openwhisk.core.entity._
3331
import org.apache.openwhisk.core.entity.types.EntityStore
3432
import org.apache.openwhisk.http.ErrorResponse.terminate
3533
import org.apache.openwhisk.http.Messages
34+
import org.apache.openwhisk.http.Messages._
35+
import pureconfig._
36+
import org.apache.openwhisk.core.ConfigKeys
3637

3738
trait WhiskPackagesApi extends WhiskCollectionAPI with ReferencedEntities {
3839
services: WhiskServices =>
@@ -42,6 +43,10 @@ trait WhiskPackagesApi extends WhiskCollectionAPI with ReferencedEntities {
4243
/** Database service to CRUD packages. */
4344
protected val entityStore: EntityStore
4445

46+
/** Config flag for Execute Only for Shared Packages */
47+
protected def executeOnly =
48+
loadConfigOrThrow[Boolean](ConfigKeys.sharedPackageExecuteOnly)
49+
4550
/** Notification service for cache invalidation. */
4651
protected implicit val cacheChangeNotification: Some[CacheChangeNotification]
4752

@@ -157,7 +162,14 @@ trait WhiskPackagesApi extends WhiskCollectionAPI with ReferencedEntities {
157162
*/
158163
override def fetch(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(
159164
implicit transid: TransactionId) = {
160-
getEntity(WhiskPackage.get(entityStore, entityName.toDocId), Some { mergePackageWithBinding() _ })
165+
if (executeOnly && user.namespace.name != entityName.namespace) {
166+
val value = entityName.toString
167+
terminate(Forbidden, forbiddenGetPackage(entityName.asString))
168+
} else {
169+
getEntity(WhiskPackage.get(entityStore, entityName.toDocId), Some {
170+
mergePackageWithBinding() _
171+
})
172+
}
161173
}
162174

163175
/**
@@ -303,9 +315,17 @@ trait WhiskPackagesApi extends WhiskCollectionAPI with ReferencedEntities {
303315
logging.error(this, s"unexpected package binding refers to itself: $docid")
304316
terminate(UnprocessableEntity, Messages.packageBindingCircularReference(b.fullyQualifiedName.toString))
305317
} else {
306-
getEntity(WhiskPackage.get(entityStore, docid), Some {
307-
mergePackageWithBinding(Some { wp }) _
308-
})
318+
319+
/** Here's where I check package execute only case with package binding. */
320+
if (executeOnly && wp.namespace.asString != b.namespace.asString) {
321+
terminate(Forbidden, forbiddenGetPackageBinding(wp.name.asString))
322+
} else {
323+
getEntity(WhiskPackage.get(entityStore, docid), Some {
324+
mergePackageWithBinding(Some {
325+
wp
326+
}) _
327+
})
328+
}
309329
}
310330
} getOrElse {
311331
val pkg = ref map { _ inherit wp.parameters } getOrElse wp

tests/src/test/scala/org/apache/openwhisk/core/controller/test/PackageActionsApiTests.scala

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,6 @@ class PackageActionsApiTests extends ControllerTestCommon with WhiskActionsApi {
187187
status should be(BadRequest)
188188
}
189189
}
190-
191190
it should "reject put action in package binding" in {
192191
implicit val tid = transid()
193192
val provider = WhiskPackage(namespace, aname(), None, publish = true)
@@ -636,4 +635,37 @@ class PackageActionsApiTests extends ControllerTestCommon with WhiskActionsApi {
636635
responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity
637636
}
638637
}
638+
639+
var testExecuteOnly = false
640+
override def executeOnly = testExecuteOnly
641+
642+
it should ("allow access to get of action in binding of shared package when config option is disabled") in {
643+
testExecuteOnly = false
644+
implicit val tid = transid()
645+
val auser = WhiskAuthHelpers.newIdentity()
646+
val provider = WhiskPackage(namespace, aname(), None, Parameters("p", "P"), publish = true)
647+
val binding = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind, Parameters("b", "B"))
648+
val action = WhiskAction(provider.fullPath, aname(), jsDefault("??"), Parameters("a", "A"))
649+
put(entityStore, provider)
650+
put(entityStore, binding)
651+
put(entityStore, action)
652+
Get(s"$collectionPath/${binding.name}/${action.name}") ~> Route.seal(routes(auser)) ~> check {
653+
status should be(OK)
654+
}
655+
}
656+
657+
it should ("deny access to get of action in binding of shared package when config option is enabled") in {
658+
testExecuteOnly = true
659+
implicit val tid = transid()
660+
val auser = WhiskAuthHelpers.newIdentity()
661+
val provider = WhiskPackage(namespace, aname(), None, Parameters("p", "P"), publish = true)
662+
val binding = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind, Parameters("b", "B"))
663+
val action = WhiskAction(provider.fullPath, aname(), jsDefault("??"), Parameters("a", "A"))
664+
put(entityStore, provider)
665+
put(entityStore, binding)
666+
put(entityStore, action)
667+
Get(s"$collectionPath/${binding.name}/${action.name}") ~> Route.seal(routes(auser)) ~> check {
668+
status should be(Forbidden)
669+
}
670+
}
639671
}

tests/src/test/scala/org/apache/openwhisk/core/controller/test/PackagesApiTests.scala

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,4 +884,58 @@ class PackagesApiTests extends ControllerTestCommon with WhiskPackagesApi {
884884
responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity
885885
}
886886
}
887+
888+
var testExecuteOnly = false
889+
override def executeOnly = testExecuteOnly
890+
891+
it should ("allow access to get of shared package binding when config option is disabled") in {
892+
testExecuteOnly = false
893+
implicit val tid = transid()
894+
val auser = WhiskAuthHelpers.newIdentity()
895+
val provider = WhiskPackage(namespace, aname(), None, Parameters("p", "P"), publish = true)
896+
val binding = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind, Parameters("b", "B"))
897+
put(entityStore, provider)
898+
put(entityStore, binding)
899+
Get(s"/$namespace/${collection.path}/${provider.name}") ~> Route.seal(routes(auser)) ~> check {
900+
status should be(OK)
901+
}
902+
}
903+
904+
it should ("allow access to get of shared package when config option is disabled") in {
905+
testExecuteOnly = false
906+
implicit val tid = transid()
907+
val auser = WhiskAuthHelpers.newIdentity()
908+
val provider = WhiskPackage(namespace, aname(), None, publish = true)
909+
put(entityStore, provider)
910+
911+
Get(s"/$namespace/${collection.path}/${provider.name}") ~> Route.seal(routes(auser)) ~> check {
912+
status should be(OK)
913+
}
914+
}
915+
916+
it should ("deny access to get of shared package binding when config option is enabled") in {
917+
testExecuteOnly = true
918+
implicit val tid = transid()
919+
val auser = WhiskAuthHelpers.newIdentity()
920+
val provider = WhiskPackage(namespace, aname(), None, Parameters("p", "P"), publish = true)
921+
val binding = WhiskPackage(EntityPath(auser.subject.asString), aname(), provider.bind, Parameters("b", "B"))
922+
put(entityStore, provider)
923+
put(entityStore, binding)
924+
Get(s"/$namespace/${collection.path}/${provider.name}") ~> Route.seal(routes(auser)) ~> check {
925+
status should be(Forbidden)
926+
}
927+
928+
}
929+
930+
it should ("deny access to get of shared package when config option is enabled") in {
931+
testExecuteOnly = true
932+
implicit val tid = transid()
933+
val auser = WhiskAuthHelpers.newIdentity()
934+
val provider = WhiskPackage(namespace, aname(), None, publish = true)
935+
put(entityStore, provider)
936+
937+
Get(s"/$namespace/${collection.path}/${provider.name}") ~> Route.seal(routes(auser)) ~> check {
938+
status should be(Forbidden)
939+
}
940+
}
887941
}

0 commit comments

Comments
 (0)