Skip to content

Commit 5356f5a

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 5356f5a

File tree

5 files changed

+158
-26
lines changed

5 files changed

+158
-26
lines changed

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: 28 additions & 11 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(_) =>
196197
putEntity(WhiskAction, entityStore, entityName.toDocId, overwrite, update(user, request) _, () => {
197198
make(user, entityName, request)
198-
})
199+
}, unlock = unlock)
199200
case Failure(f) =>
200201
super.handleEntitlementFailure(f)
201202
}
@@ -518,16 +519,32 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
518519

519520
val exec = content.exec getOrElse action.exec
520521

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

533550
/**

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)