blob: 57b9f31e85f7b4a145a9038974113e0af0f27b86 [file] [log] [blame]
/*
* 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 org.apache.openwhisk.core.entity
import org.apache.openwhisk.common.{Logging, TransactionId}
import org.apache.openwhisk.core.database.{
MultipleReadersSingleWriterCache,
NoDocumentException,
StaleParameter,
WriteTime
}
import org.apache.openwhisk.core.entitlement.Privilege
import org.apache.openwhisk.core.entity.types.AuthStore
import spray.json._
import scala.concurrent.Future
import scala.util.Try
case class UserLimits(invocationsPerMinute: Option[Int] = None,
concurrentInvocations: Option[Int] = None,
firesPerMinute: Option[Int] = None,
allowedKinds: Option[Set[String]] = None,
storeActivations: Option[Boolean] = None)
object UserLimits extends DefaultJsonProtocol {
val standardUserLimits = UserLimits()
implicit val serdes = jsonFormat5(UserLimits.apply)
}
protected[core] case class Namespace(name: EntityName, uuid: UUID)
protected[core] object Namespace extends DefaultJsonProtocol {
implicit val serdes = jsonFormat2(Namespace.apply)
}
protected[core] case class Identity(subject: Subject,
namespace: Namespace,
authkey: GenericAuthKey,
rights: Set[Privilege] = Set.empty,
limits: UserLimits = UserLimits.standardUserLimits)
object Identity extends MultipleReadersSingleWriterCache[Option[Identity], DocInfo] with DefaultJsonProtocol {
private val viewName = WhiskQueries.view(WhiskQueries.dbConfig.subjectsDdoc, "identities").name
override val cacheEnabled = true
override val evictionPolicy = WriteTime
// upper bound for the auth cache to prevent memory pollution by sending
// malicious namespace patterns
override val fixedCacheSize = 100000
implicit val serdes = jsonFormat5(Identity.apply)
/**
* Retrieves a key for namespace.
* There may be more than one key for the namespace, in which case,
* one is picked arbitrarily.
*/
def get(datastore: AuthStore, namespace: EntityName)(implicit transid: TransactionId): Future[Identity] = {
implicit val logger: Logging = datastore.logging
implicit val ec = datastore.executionContext
val ns = namespace.asString
val key = CacheKey(namespace)
cacheLookup(
key, {
list(datastore, List(ns), limit = 1) map { list =>
list.length match {
case 1 =>
Some(rowToIdentity(list.head, ns))
case 0 =>
logger.info(this, s"$viewName[$namespace] does not exist")
None
case _ =>
logger.error(this, s"$viewName[$namespace] is not unique")
throw new IllegalStateException("namespace is not unique")
}
}
}).map(_.getOrElse(throw new NoDocumentException("namespace does not exist")))
}
def get(datastore: AuthStore, authkey: BasicAuthenticationAuthKey)(
implicit transid: TransactionId): Future[Identity] = {
implicit val logger: Logging = datastore.logging
implicit val ec = datastore.executionContext
cacheLookup(
CacheKey(authkey), {
list(datastore, List(authkey.uuid.asString, authkey.key.asString)) map { list =>
list.length match {
case 1 =>
Some(rowToIdentity(list.head, authkey.uuid.asString))
case 0 =>
logger.info(this, s"$viewName[${authkey.uuid}] does not exist")
None
case _ =>
logger.error(this, s"$viewName[${authkey.uuid}] is not unique")
throw new IllegalStateException("uuid is not unique")
}
}
}).map(_.getOrElse(throw new NoDocumentException("namespace does not exist")))
}
def list(datastore: AuthStore, key: List[Any], limit: Int = 2)(
implicit transid: TransactionId): Future[List[JsObject]] = {
datastore.query(
viewName,
startKey = key,
endKey = key,
skip = 0,
limit = limit,
includeDocs = true,
descending = true,
reduce = false,
stale = StaleParameter.No)
}
protected[entity] def rowToIdentity(row: JsObject, key: String)(implicit transid: TransactionId, logger: Logging) = {
row.getFields("id", "value", "doc") match {
case Seq(JsString(id), JsObject(value), doc) =>
val limits =
if (doc != JsNull) Try(doc.convertTo[UserLimits]).getOrElse(UserLimits.standardUserLimits)
else UserLimits.standardUserLimits
val subject = Subject(id)
val JsString(uuid) = value("uuid")
val JsString(secret) = value("key")
val JsString(namespace) = value("namespace")
Identity(
subject,
Namespace(EntityName(namespace), UUID(uuid)),
BasicAuthenticationAuthKey(UUID(uuid), Secret(secret)),
Privilege.ALL,
limits)
case _ =>
logger.error(this, s"$viewName[$key] has malformed view '${row.compactPrint}'")
throw new IllegalStateException("identities view malformed")
}
}
}