Skip to content

Commit d95dfce

Browse files
committed
Add protect feature to avoid update or delete actions by mistake
As more and more production system uses openwhisk, Users will need some features to protect their service safe from mistake. If we have this feature, user can protect their actions from updating or deletion by mistake.
1 parent f7afa71 commit d95dfce

5 files changed

Lines changed: 166 additions & 32 deletions

File tree

common/scala/src/main/scala/whisk/core/entity/WhiskAction.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ case class WhiskActionPut(exec: Option[Exec] = None,
5252
limits: Option[ActionLimitsOption] = None,
5353
version: Option[SemVer] = None,
5454
publish: Option[Boolean] = None,
55-
annotations: Option[Parameters] = None) {
55+
annotations: Option[Parameters] = None,
56+
unlock: Option[Boolean] = None) {
5657

5758
protected[core] def replace(exec: Exec) = {
5859
WhiskActionPut(Some(exec), parameters, limits, version, publish, annotations)
@@ -305,6 +306,7 @@ case class ExecutableWhiskActionMetaData(namespace: EntityPath,
305306
object WhiskAction extends DocumentFactory[WhiskAction] with WhiskEntityQueries[WhiskAction] with DefaultJsonProtocol {
306307

307308
val execFieldName = "exec"
309+
val lockFieldName = "lock"
308310
val finalParamsAnnotationName = "final"
309311

310312
override val collectionName = "actions"
@@ -589,5 +591,5 @@ object ActionLimitsOption extends DefaultJsonProtocol {
589591
}
590592

591593
object WhiskActionPut extends DefaultJsonProtocol {
592-
implicit val serdes = jsonFormat6(WhiskActionPut.apply)
594+
implicit val serdes = jsonFormat7(WhiskActionPut.apply)
593595
}

core/controller/src/main/scala/whisk/core/controller/Actions.scala

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,13 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
190190
val checkAdditionalPrivileges = entitleReferencedEntities(user, Privilege.READ, request.exec).flatMap {
191191
case _ => entitlementProvider.check(user, content.exec)
192192
}
193+
val unlock = content.unlock.getOrElse(false)
193194

194195
onComplete(checkAdditionalPrivileges) {
195196
case Success(_) =>
196-
putEntity(WhiskAction, entityStore, entityName.toDocId, overwrite, update(user, request) _, () => {
197+
putEntity(WhiskAction, entityStore, entityName.toDocId, overwrite, update(user, request, unlock) _, () => {
197198
make(user, entityName, request)
198-
})
199+
}, unlock = unlock)
199200
case Failure(f) =>
200201
super.handleEntitlementFailure(f)
201202
}
@@ -454,20 +455,21 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
454455
}
455456

456457
/** Updates a WhiskAction from PUT content, merging old action where necessary. */
457-
private def update(user: Identity, content: WhiskActionPut)(action: WhiskAction)(implicit transid: TransactionId) = {
458+
private def update(user: Identity, content: WhiskActionPut, unlock: Boolean)(action: WhiskAction)(
459+
implicit transid: TransactionId) = {
458460
content.exec map {
459461
case seq: SequenceExec =>
460462
// check that the sequence conforms to max length and no recursion rules
461463
checkSequenceActionLimits(FullyQualifiedEntityName(action.namespace, action.name), seq.components) map { _ =>
462-
updateWhiskAction(content.replace(seq), action)
464+
updateWhiskAction(content.replace(seq), action, unlock)
463465
}
464466
case supportedExec if !supportedExec.deprecated =>
465-
Future successful updateWhiskAction(content, action)
467+
Future successful updateWhiskAction(content, action, unlock)
466468
case deprecatedExec =>
467469
Future failed RejectRequest(BadRequest, runtimeDeprecated(deprecatedExec))
468470
} getOrElse {
469471
if (!action.exec.deprecated) {
470-
Future successful updateWhiskAction(content, action)
472+
Future successful updateWhiskAction(content, action, unlock)
471473
} else {
472474
Future failed RejectRequest(BadRequest, runtimeDeprecated(action.exec))
473475
}
@@ -477,7 +479,8 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
477479
/**
478480
* Updates a WhiskAction instance from the PUT request.
479481
*/
480-
private def updateWhiskAction(content: WhiskActionPut, action: WhiskAction)(implicit transid: TransactionId) = {
482+
private def updateWhiskAction(content: WhiskActionPut, action: WhiskAction, unlock: Boolean)(
483+
implicit transid: TransactionId) = {
481484
val limits = content.limits map { l =>
482485
ActionLimits(
483486
l.timeout getOrElse action.limits.timeout,
@@ -518,16 +521,32 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
518521

519522
val exec = content.exec getOrElse action.exec
520523

521-
WhiskAction(
522-
action.namespace,
523-
action.name,
524-
exec,
525-
parameters,
526-
limits,
527-
content.version getOrElse action.version.upPatch,
528-
content.publish getOrElse action.publish,
529-
(content.annotations getOrElse action.annotations) ++ execAnnotation(exec))
530-
.revision[WhiskAction](action.docinfo.rev)
524+
if (unlock) {
525+
WhiskAction(
526+
action.namespace,
527+
action.name,
528+
exec,
529+
parameters,
530+
limits,
531+
content.version getOrElse action.version.upPatch,
532+
content.publish getOrElse action.publish,
533+
(content.annotations getOrElse action.annotations) ++ execAnnotation(exec) ++ Parameters(
534+
WhiskAction.lockFieldName,
535+
JsBoolean(false)))
536+
.revision[WhiskAction](action.docinfo.rev)
537+
} else {
538+
//keep original value not changed
539+
WhiskAction(
540+
action.namespace,
541+
action.name,
542+
exec,
543+
parameters,
544+
limits,
545+
content.version getOrElse action.version.upPatch,
546+
content.publish getOrElse action.publish,
547+
(content.annotations getOrElse action.annotations) ++ execAnnotation(exec))
548+
.revision[WhiskAction](action.docinfo.rev)
549+
}
531550
}
532551

533552
/**

core/controller/src/main/scala/whisk/core/controller/ApiUtils.scala

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@ import scala.util.Failure
2323
import scala.util.Success
2424
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
2525
import akka.http.scaladsl.model.StatusCode
26-
import akka.http.scaladsl.model.StatusCodes.Conflict
27-
import akka.http.scaladsl.model.StatusCodes.InternalServerError
28-
import akka.http.scaladsl.model.StatusCodes.NotFound
29-
import akka.http.scaladsl.model.StatusCodes.OK
26+
import akka.http.scaladsl.model.StatusCodes._
3027
import akka.http.scaladsl.server.{Directives, RequestContext, RouteResult}
3128
import spray.json.DefaultJsonProtocol._
3229
import spray.json.JsObject
@@ -36,8 +33,7 @@ import whisk.common.Logging
3633
import whisk.common.TransactionId
3734
import whisk.core.controller.PostProcess.PostProcessEntity
3835
import whisk.core.database._
39-
import whisk.core.entity.DocId
40-
import whisk.core.entity.WhiskDocument
36+
import whisk.core.entity.{DocId, WhiskAction, WhiskDocument}
4137
import whisk.http.ErrorResponse
4238
import whisk.http.ErrorResponse.terminate
4339
import whisk.http.Messages._
@@ -242,7 +238,8 @@ trait WriteOps extends Directives {
242238
update: A => Future[A],
243239
create: () => Future[A],
244240
treatExistsAsConflict: Boolean = true,
245-
postProcess: Option[PostProcessEntity[A]] = None)(
241+
postProcess: Option[PostProcessEntity[A]] = None,
242+
unlock: Boolean = false)(
246243
implicit transid: TransactionId,
247244
format: RootJsonFormat[A],
248245
notifier: Option[CacheChangeNotification],
@@ -267,8 +264,24 @@ trait WriteOps extends Directives {
267264
} flatMap {
268265
case (old, a) =>
269266
logging.debug(this, s"[PUT] entity created/updated, writing back to datastore")
270-
factory.put(datastore, a, old) map { _ =>
271-
a
267+
if (overwrite && !unlock && old.getOrElse(None).isInstanceOf[WhiskAction]) {
268+
val oldWhiskAction = old.getOrElse(None).asInstanceOf[WhiskAction]
269+
oldWhiskAction.annotations.get(WhiskAction.lockFieldName) match {
270+
case Some(value) if (value.convertTo[Boolean]) => {
271+
Future failed RejectRequest(
272+
MethodNotAllowed,
273+
s"this action can't be updated until ${WhiskAction.lockFieldName} annotation is updated to false")
274+
}
275+
case _ => {
276+
factory.put(datastore, a, old) map { _ =>
277+
a
278+
}
279+
}
280+
}
281+
} else {
282+
factory.put(datastore, a, old) map { _ =>
283+
a
284+
}
272285
}
273286
}) {
274287
case Success(entity) =>
@@ -322,11 +335,30 @@ trait WriteOps extends Directives {
322335
notifier: Option[CacheChangeNotification],
323336
ma: Manifest[A]) = {
324337
onComplete(factory.get(datastore, docid) flatMap { entity =>
325-
confirm(entity) flatMap {
326-
case _ =>
327-
factory.del(datastore, entity.docinfo) map { _ =>
328-
entity
338+
if (entity.isInstanceOf[WhiskAction]) {
339+
val whiskAction = entity.asInstanceOf[WhiskAction]
340+
whiskAction.annotations.get(WhiskAction.lockFieldName) match {
341+
case Some(value) if (value.convertTo[Boolean]) => {
342+
Future failed RejectRequest(
343+
MethodNotAllowed,
344+
s"this action can't be deleted until ${WhiskAction.lockFieldName} annotation is updated to false")
345+
}
346+
case _ => {
347+
confirm(entity) flatMap {
348+
case _ =>
349+
factory.del(datastore, entity.docinfo) map { _ =>
350+
entity
351+
}
352+
}
329353
}
354+
}
355+
} else {
356+
confirm(entity) flatMap {
357+
case _ =>
358+
factory.del(datastore, entity.docinfo) map { _ =>
359+
entity
360+
}
361+
}
330362
}
331363
}) {
332364
case Success(entity) =>

docs/actions.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,23 @@ You can clean up by deleting actions that you do not want to use.
596596
actions
597597
/guest/mySequence private sequence
598598
```
599+
## Protect action updated or deleted by mistake
600+
601+
If your action is very important, you can add `--annotation lock true` to protect it
602+
```
603+
wsk action create greeting greeting.js --annotation lock true
604+
```
605+
Then update or delete this action will be not allowed
606+
607+
You can add `{"unlock":true}` to enable `update or delete operation`, for example:
608+
```
609+
curl -X PUT -H "Content-type: application/json"
610+
--user 23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP
611+
-d '{"unlock":true}'
612+
'/api/v1/namespaces/guest/actions/hello?overwrite=true'
613+
```
614+
615+
TODO(add unlock operation to wsk, for example: wsk action update hello --unlock)
599616

600617
## Accessing action metadata within the action body
601618

tests/src/test/scala/whisk/core/controller/test/ActionsApiTests.scala

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,70 @@ class ActionsApiTests extends ControllerTestCommon with WhiskActionsApi {
401401
}
402402
}
403403

404+
it should "update action not allowed when lock annotation is true" in {
405+
implicit val tid = transid()
406+
val action =
407+
WhiskAction(
408+
namespace,
409+
aname(),
410+
jsDefault(""),
411+
annotations = Parameters(WhiskAction.lockFieldName, JsBoolean(true)))
412+
val content = JsObject(
413+
"exec" -> JsObject("code" -> "".toJson, "kind" -> action.exec.kind.toJson),
414+
"annotations" -> action.annotations.toJson)
415+
Put(s"$collectionPath/${action.name}", content) ~> Route.seal(routes(creds)) ~> check {
416+
status should be(OK)
417+
}
418+
419+
//Update not allowed
420+
Put(s"$collectionPath/${action.name}?overwrite=true", content) ~> Route.seal(routes(creds)) ~> check {
421+
status should be(MethodNotAllowed)
422+
}
423+
424+
//unlock the action
425+
val unlockContent = s"""{"unlock":true}""".stripMargin.parseJson.asJsObject
426+
Put(s"$collectionPath/${action.name}?overwrite=true", unlockContent) ~> Route.seal(routes(creds)) ~> check {
427+
status should be(OK)
428+
}
429+
430+
//Update allowed after unlock the action
431+
Put(s"$collectionPath/${action.name}?overwrite=true", content) ~> Route.seal(routes(creds)) ~> check {
432+
status should be(OK)
433+
}
434+
}
435+
436+
it should "delete action not allowed when lock annotation is true" in {
437+
implicit val tid = transid()
438+
val action =
439+
WhiskAction(
440+
namespace,
441+
aname(),
442+
jsDefault(""),
443+
annotations = Parameters(WhiskAction.lockFieldName, JsBoolean(true)))
444+
val content = JsObject(
445+
"exec" -> JsObject("code" -> "".toJson, "kind" -> action.exec.kind.toJson),
446+
"annotations" -> action.annotations.toJson)
447+
Put(s"$collectionPath/${action.name}", content) ~> Route.seal(routes(creds)) ~> check {
448+
status should be(OK)
449+
}
450+
451+
//Delete not allowed
452+
Delete(s"$collectionPath/${action.name}") ~> Route.seal(routes(creds)) ~> check {
453+
status should be(MethodNotAllowed)
454+
}
455+
456+
//unlock the action
457+
val unlockContent = s"""{"unlock":true}""".stripMargin.parseJson.asJsObject
458+
Put(s"$collectionPath/${action.name}?overwrite=true", unlockContent) ~> Route.seal(routes(creds)) ~> check {
459+
status should be(OK)
460+
}
461+
462+
//Delete allowed after unlock the action
463+
Delete(s"$collectionPath/${action.name}") ~> Route.seal(routes(creds)) ~> check {
464+
status should be(OK)
465+
}
466+
}
467+
404468
it should "report NotFound for delete non existent action" in {
405469
implicit val tid = transid()
406470
Delete(s"$collectionPath/xyz") ~> Route.seal(routes(creds)) ~> check {

0 commit comments

Comments
 (0)