Skip to content

Commit db02599

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. - Apply unix style permission management - Make the action's downloadable for the shared user
1 parent 66a9417 commit db02599

File tree

6 files changed

+473
-49
lines changed

6 files changed

+473
-49
lines changed

common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskAction.scala

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ case class WhiskActionPut(exec: Option[Exec] = None,
7676
case _ => this
7777
} getOrElse this
7878
}
79+
80+
protected[core] def getPermissions(): Option[String] = {
81+
annotations match {
82+
case Some(value) =>
83+
value
84+
.get(WhiskAction.permissionsFieldName)
85+
.map(value => value.convertTo[String])
86+
case None => None
87+
}
88+
}
7989
}
8090

8191
abstract class WhiskActionLike(override val name: EntityName) extends WhiskEntity(name, "action") {
@@ -351,6 +361,49 @@ object WhiskAction extends DocumentFactory[WhiskAction] with WhiskEntityQueries[
351361
val execFieldName = "exec"
352362
val requireWhiskAuthHeader = "x-require-whisk-auth"
353363

364+
// annotation permission key name
365+
val permissionsFieldName = "permissions"
366+
367+
val defaultPermissions = "rwxr-x"
368+
369+
// notes on users, just have 2 type users,
370+
// 1. the action's owner
371+
// 2. the user (not the owner) who used the shared action directly(e.g. get, invoke), we call it "the shared user"
372+
//
373+
// Notes on permission control
374+
// 1. the owner has read(or download) permission on any situation, but for the shared user,
375+
// in spite of has read permission on any situation, but can set it undownloadable or downloadable
376+
// 2. the shared user can't update/delete the action on any situation.
377+
// 3. the owner's permission can affect the shared user's permission, e.g
378+
// if the owner is not given execute permission, the shared user can't have execute permission as well.
379+
//
380+
// Notes on permission values, include below permission value
381+
// 1. permission code:rwxr-x: owner:read(yes)/write(yes)/execute(yes)|the shared action's user:download(yes)/write(no)/execute(yes), this is default
382+
// 2. permission code:rwxr--: owner:read(yes)/write(yes)/execute(yes)|the shared action's user:download(yes)/write(no)/execute(no)
383+
// 3. permission code:r-xr-x: owner:read(yes)/write(no)/execute(yes)|the shared action's user:download(yes)/write(no)/execute(yes)
384+
// 4. permission code:r-xr--: owner:read(yes)/write(no)/execute(yes)|the shared action's user:download(yes)/write(no)/execute(no)
385+
// 5. permission code:r--r--: owner:read(yes)/write(no)/execute(no)|the shared action's user:download(yes)/write(no)/execute(no)
386+
// 6. permission code:rw-r--: owner:read(yes)/write(yes)/execute(no)|the shared action's user:download(yes)/write(no)/execute(no)
387+
// 7. permission code:rwx--x: owner:read(yes)/write(yes)/execute(yes)|the shared action's user:download(no)/write(no)/execute(yes)
388+
// 8. permission code:rwx---: owner:read(yes)/write(yes)/execute(yes)|the shared action's user:download(no)/write(no)/execute(no)
389+
// 9. permission code:r-x--x: owner:read(yes)/write(no)/execute(yes)|the shared action's user:download(no)/write(no)/execute(yes)
390+
// 10. permission code:r-x---: owner:read(yes)/write(no)/execute(yes)|the shared action's user:download(no)/write(no)/execute(no)
391+
// 11. permission code:r-----: owner:read(yes)/write(no)/execute(no)|the shared action's user:download(no)/write(no)/execute(no)
392+
// 12. permission code:rw----: owner:read(yes)/write(yes)/execute(no)|the shared action's user:download(no)/write(no)/execute(no)
393+
val permissionList = List(
394+
defaultPermissions,
395+
"rwxr--",
396+
"r-xr-x",
397+
"r-xr--",
398+
"r--r--",
399+
"rw-r--",
400+
"rwx--x",
401+
"rwx---",
402+
"r-x--x",
403+
"r-x---",
404+
"r-----",
405+
"rw----")
406+
354407
override val collectionName = "actions"
355408
override val cacheEnabled = true
356409

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,16 @@ package org.apache.openwhisk.http
2020
import scala.concurrent.duration.Duration
2121
import scala.concurrent.duration.FiniteDuration
2222
import scala.util.Try
23-
2423
import akka.http.scaladsl.model.StatusCode
2524
import akka.http.scaladsl.model.StatusCodes.Forbidden
2625
import akka.http.scaladsl.model.StatusCodes.NotFound
2726
import akka.http.scaladsl.model.MediaType
2827
import akka.http.scaladsl.server.Directives
2928
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonMarshaller
3029
import akka.http.scaladsl.server.StandardRoute
31-
3230
import spray.json._
33-
3431
import org.apache.openwhisk.common.TransactionId
35-
import org.apache.openwhisk.core.entity.SizeError
36-
import org.apache.openwhisk.core.entity.ByteSize
37-
import org.apache.openwhisk.core.entity.Exec
38-
import org.apache.openwhisk.core.entity.ExecMetaDataBase
39-
import org.apache.openwhisk.core.entity.ActivationId
32+
import org.apache.openwhisk.core.entity.{ActivationId, ByteSize, Exec, ExecMetaDataBase, SizeError}
4033

4134
object Messages {
4235

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

Lines changed: 72 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,22 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
218218

219219
onComplete(checkAdditionalPrivileges) {
220220
case Success(_) =>
221-
putEntity(WhiskAction, entityStore, entityName.toDocId, overwrite, update(user, request) _, () => {
222-
make(user, entityName, request)
223-
})
221+
val operation = if (overwrite) "update" else "create"
222+
onComplete(
223+
entitlementProvider
224+
.checkActionPermissions(
225+
operation,
226+
user,
227+
entityStore,
228+
entityName,
229+
WhiskAction.get,
230+
content.getPermissions())) {
231+
case Success(_) =>
232+
putEntity(WhiskAction, entityStore, entityName.toDocId, overwrite, update(user, request) _, () => {
233+
make(user, entityName, request)
234+
})
235+
case Failure(f) => super.handleEntitlementFailure(f)
236+
}
224237
case Failure(f) =>
225238
super.handleEntitlementFailure(f)
226239
}
@@ -241,37 +254,43 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
241254
*/
242255
override def activate(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters])(
243256
implicit transid: TransactionId) = {
244-
parameter(
245-
'blocking ? false,
246-
'result ? false,
247-
'timeout.as[FiniteDuration] ? controllerActivationConfig.maxWaitForBlockingActivation) {
248-
(blocking, result, waitOverride) =>
249-
entity(as[Option[JsObject]]) { payload =>
250-
getEntity(WhiskActionMetaData.resolveActionAndMergeParameters(entityStore, entityName), Some {
251-
act: WhiskActionMetaData =>
252-
// resolve the action --- special case for sequences that may contain components with '_' as default package
253-
val action = act.resolve(user.namespace)
254-
onComplete(entitleReferencedEntitiesMetaData(user, Privilege.ACTIVATE, Some(action.exec))) {
255-
case Success(_) =>
256-
val actionWithMergedParams = env.map(action.inherit(_)) getOrElse action
257-
258-
// incoming parameters may not override final parameters (i.e., parameters with already defined values)
259-
// on an action once its parameters are resolved across package and binding
260-
val allowInvoke = payload
261-
.map(_.fields.keySet.forall(key => !actionWithMergedParams.immutableParameters.contains(key)))
262-
.getOrElse(true)
257+
onComplete(
258+
entitlementProvider
259+
.checkActionPermissions("invoke", user, entityStore, entityName, WhiskAction.get)) {
260+
case Success(_) =>
261+
parameter(
262+
'blocking ? false,
263+
'result ? false,
264+
'timeout.as[FiniteDuration] ? controllerActivationConfig.maxWaitForBlockingActivation) {
265+
(blocking, result, waitOverride) =>
266+
entity(as[Option[JsObject]]) { payload =>
267+
getEntity(WhiskActionMetaData.resolveActionAndMergeParameters(entityStore, entityName), Some {
268+
act: WhiskActionMetaData =>
269+
// resolve the action --- special case for sequences that may contain components with '_' as default package
270+
val action = act.resolve(user.namespace)
271+
onComplete(entitleReferencedEntitiesMetaData(user, Privilege.ACTIVATE, Some(action.exec))) {
272+
case Success(_) =>
273+
val actionWithMergedParams = env.map(action.inherit(_)) getOrElse action
274+
275+
// incoming parameters may not override final parameters (i.e., parameters with already defined values)
276+
// on an action once its parameters are resolved across package and binding
277+
val allowInvoke = payload
278+
.map(_.fields.keySet.forall(key => !actionWithMergedParams.immutableParameters.contains(key)))
279+
.getOrElse(true)
280+
281+
if (allowInvoke) {
282+
doInvoke(user, actionWithMergedParams, payload, blocking, waitOverride, result)
283+
} else {
284+
terminate(BadRequest, Messages.parametersNotAllowed)
285+
}
263286

264-
if (allowInvoke) {
265-
doInvoke(user, actionWithMergedParams, payload, blocking, waitOverride, result)
266-
} else {
267-
terminate(BadRequest, Messages.parametersNotAllowed)
287+
case Failure(f) =>
288+
super.handleEntitlementFailure(f)
268289
}
269-
270-
case Failure(f) =>
271-
super.handleEntitlementFailure(f)
272-
}
273-
})
290+
})
291+
}
274292
}
293+
case Failure(f) => super.handleEntitlementFailure(f)
275294
}
276295
}
277296

@@ -333,11 +352,17 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
333352
* - 500 Internal Server Error
334353
*/
335354
override def remove(user: Identity, entityName: FullyQualifiedEntityName)(implicit transid: TransactionId) = {
336-
deleteEntity(WhiskAction, entityStore, entityName.toDocId, (a: WhiskAction) => Future.successful({}))
355+
onComplete(
356+
entitlementProvider
357+
.checkActionPermissions("remove", user, entityStore, entityName, WhiskAction.get)) {
358+
case Success(_) =>
359+
deleteEntity(WhiskAction, entityStore, entityName.toDocId, (a: WhiskAction) => Future.successful({}))
360+
case Failure(f) => super.handleEntitlementFailure(f)
361+
}
337362
}
338363

339364
/** 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)(
365+
private def fetchEntity(user: Identity, entityName: FullyQualifiedEntityName, env: Option[Parameters], code: Boolean)(
341366
implicit transid: TransactionId) = {
342367
val resolvedPkg: Future[Either[String, FullyQualifiedEntityName]] = if (entityName.path.defaultPackage) {
343368
Future.successful(Right(entityName))
@@ -357,13 +382,19 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
357382
case Left(f) => terminate(Forbidden, f)
358383
case Right(_) =>
359384
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-
})
385+
onComplete(
386+
entitlementProvider
387+
.checkActionPermissions("download", user, entityStore, entityName, WhiskAction.get)) {
388+
case Success(_) =>
389+
getEntity(WhiskAction.resolveActionAndMergeParameters(entityStore, entityName), Some {
390+
action: WhiskAction =>
391+
val mergedAction = env map {
392+
action inherit _
393+
} getOrElse action
394+
complete(OK, mergedAction)
395+
})
396+
case Failure(f) => super.handleEntitlementFailure(f)
397+
}
367398
} else {
368399
getEntity(WhiskActionMetaData.resolveActionAndMergeParameters(entityStore, entityName), Some {
369400
action: WhiskActionMetaData =>
@@ -396,7 +427,7 @@ trait WhiskActionsApi extends WhiskCollectionAPI with PostActionActivation with
396427
if (executeOnly && user.namespace.name != entityName.namespace) {
397428
terminate(Forbidden, forbiddenGetAction(entityName.path.asString))
398429
} else {
399-
fetchEntity(entityName, env, code)
430+
fetchEntity(user, entityName, env, code)
400431
}
401432
}
402433
}

core/controller/src/main/scala/org/apache/openwhisk/core/entitlement/Entitlement.scala

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,12 @@ import org.apache.openwhisk.core.loadBalancer.{LoadBalancer, ShardingContainerPo
3636
import org.apache.openwhisk.http.ErrorResponse
3737
import org.apache.openwhisk.http.Messages
3838
import org.apache.openwhisk.core.connector.MessagingProvider
39+
import org.apache.openwhisk.core.database.ArtifactStore
3940
import org.apache.openwhisk.spi.SpiLoader
4041
import org.apache.openwhisk.spi.Spi
4142

43+
import spray.json.DefaultJsonProtocol._
44+
4245
object types {
4346
type Entitlements = TrieMap[(Subject, String), Set[Privilege]]
4447
}
@@ -231,6 +234,129 @@ protected[core] abstract class EntitlementProvider(
231234
.getOrElse(Future.successful(()))
232235
}
233236

237+
/**
238+
* Checks if action operation(get/write/execute) whether feasible
239+
*
240+
* @param operation the action operation, e.g. get/write/execute
241+
* @param user the user who get/write/execute the action
242+
* @param entityStore store to write the action to
243+
* @param entityName entityName
244+
* @param permissions the passed permission code
245+
* @return a promise that completes with success iff action operation is feasible
246+
*/
247+
protected[core] def checkActionPermissions(
248+
operation: String,
249+
user: Identity,
250+
entityStore: ArtifactStore[WhiskEntity],
251+
entityName: FullyQualifiedEntityName,
252+
get: (ArtifactStore[WhiskEntity], DocId, DocRevision, Boolean) => Future[WhiskAction],
253+
permissions: Option[String] = None)(implicit transid: TransactionId): Future[Unit] = {
254+
if (operation == "create") {
255+
permissions
256+
.map { value =>
257+
if (WhiskAction.permissionList.contains(value)) {
258+
Future.successful(())
259+
} else {
260+
val errorInfo =
261+
s"give error permission code: ${value}, available permission is in ${WhiskAction.permissionList}"
262+
Future.failed(RejectRequest(Forbidden, Some(ErrorResponse(errorInfo, transid))))
263+
}
264+
}
265+
.getOrElse(Future.successful(()))
266+
} else if (operation == "update") {
267+
get(entityStore, entityName.toDocId, DocRevision.empty, true).flatMap { whiskAction =>
268+
val currentPermissions = whiskAction.annotations
269+
.get(WhiskAction.permissionsFieldName)
270+
.map(value => value.convertTo[String])
271+
.getOrElse(WhiskAction.defaultPermissions)
272+
273+
val errorInfo = s"have no permission to ${operation} this action"
274+
permissions match {
275+
case Some(value) =>
276+
if (!WhiskAction.permissionList.contains(value)) {
277+
val errorInfo =
278+
s"give error permission code: ${value}, available permission is in ${WhiskAction.permissionList}"
279+
Future.failed(RejectRequest(Forbidden, Some(ErrorResponse(errorInfo, transid))))
280+
} else {
281+
val passedUpdatePermission = value.charAt(1)
282+
if (passedUpdatePermission == 'w') { // make it to modifiable
283+
Future.successful(())
284+
} else {
285+
val currentUpdatePermission = currentPermissions.charAt(1)
286+
if (currentUpdatePermission == '-') {
287+
Future.failed(RejectRequest(Forbidden, Some(ErrorResponse(errorInfo, transid))))
288+
} else {
289+
Future.successful(())
290+
}
291+
}
292+
}
293+
case None =>
294+
val currentUpdatePermission = currentPermissions.charAt(1)
295+
if (currentUpdatePermission == '-') {
296+
Future.failed(RejectRequest(Forbidden, Some(ErrorResponse(errorInfo, transid))))
297+
} else {
298+
Future.successful(())
299+
}
300+
}
301+
}
302+
} else if (operation == "remove") {
303+
get(entityStore, entityName.toDocId, DocRevision.empty, true).flatMap { whiskAction =>
304+
val currentPermissions = whiskAction.annotations
305+
.get(WhiskAction.permissionsFieldName)
306+
.map(value => value.convertTo[String])
307+
.getOrElse(WhiskAction.defaultPermissions)
308+
309+
val currentUpdatePermission = currentPermissions.charAt(1)
310+
if (currentUpdatePermission == '-') {
311+
val errorInfo = s"have no permission to ${operation} this action"
312+
Future.failed(RejectRequest(Forbidden, Some(ErrorResponse(errorInfo, transid))))
313+
} else {
314+
Future.successful(())
315+
}
316+
}
317+
} else if (operation == "invoke") {
318+
get(entityStore, entityName.toDocId, DocRevision.empty, true).flatMap { whiskAction =>
319+
val currentPermissions = whiskAction.annotations
320+
.get(WhiskAction.permissionsFieldName)
321+
.map(value => value.convertTo[String])
322+
.getOrElse(WhiskAction.defaultPermissions)
323+
324+
// the user who is owner by default
325+
var currentExecutePermission = currentPermissions.charAt(2)
326+
var errorInfo = s"have no permission to ${operation} this action"
327+
if (user.namespace.name.asString != entityName.path.root.asString) { // the user who invoke the shared action
328+
currentExecutePermission = currentPermissions.charAt(5)
329+
errorInfo = s"have no permission to ${operation} this shared action"
330+
}
331+
if (currentExecutePermission == '-') { //have no permission
332+
Future.failed(RejectRequest(Forbidden, Some(ErrorResponse(errorInfo, transid))))
333+
} else {
334+
Future.successful(())
335+
}
336+
}
337+
} else { // download the code
338+
get(entityStore, entityName.toDocId, DocRevision.empty, true).flatMap { whiskAction =>
339+
val currentPermissions = whiskAction.annotations
340+
.get(WhiskAction.permissionsFieldName)
341+
.map(value => value.convertTo[String])
342+
.getOrElse(WhiskAction.defaultPermissions)
343+
344+
val errorInfo = s"have no permission to download this shared action"
345+
val currentDownloadPermission = currentPermissions.charAt(3)
346+
if (user.namespace.name.asString != entityName.path.root.asString) { // the shared user who download the action
347+
if (currentDownloadPermission == '-') {
348+
Future.failed(RejectRequest(Forbidden, Some(ErrorResponse(errorInfo, transid))))
349+
} else {
350+
Future.successful(())
351+
}
352+
} else {
353+
// the owner has download permission on any situation
354+
Future.successful(())
355+
}
356+
}
357+
}
358+
}
359+
234360
/**
235361
* Checks if a subject has the right to access a specific resource. The entitlement may be implicit,
236362
* that is, inferred based on namespaces that a subject belongs to and the namespace of the

0 commit comments

Comments
 (0)