blob: bfeee598d1d1733037eaa5d3a84834693d1fb296 [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.database
import java.util.UUID
import akka.actor.ActorSystem
import akka.stream.scaladsl.{Sink, Source}
import org.apache.openwhisk.common.{Logging, TransactionId}
import org.apache.openwhisk.core.cli.{CommandError, CommandMessages, IllegalState, WhiskCommand}
import org.apache.openwhisk.core.database.UserCommand.ExtendedAuth
import org.apache.openwhisk.core.entity._
import org.apache.openwhisk.core.entity.types._
import org.apache.openwhisk.http.Messages
import org.apache.openwhisk.spi.SpiLoader
import org.rogach.scallop.{ScallopConfBase, Subcommand}
import spray.json.{JsBoolean, JsObject, JsString, JsValue, RootJsonFormat}
import scala.concurrent.{ExecutionContext, Future}
import scala.language.reflectiveCalls
import scala.reflect.classTag
import scala.util.{Properties, Try}
class UserCommand extends Subcommand("user") with WhiskCommand {
descr("manage users")
class CreateUserCmd extends Subcommand("create") {
descr("create a user and show authorization key")
val auth =
opt[String](
descr = "the uuid:key to initialize the subject authorization key with",
argName = "AUTH",
short = 'u')
val namespace =
opt[String](descr = "create key for given namespace instead (defaults to subject id)", argName = "NAMESPACE")
val revoke =
opt[Boolean](descr = "revoke the current authorization key and generate a new key", short = 'r')
val force =
opt[Boolean](descr = "force update an existing subject authorization uuid:key", short = 'f')
val subject = trailArg[String](descr = "the subject to create")
validate(subject) { s =>
if (s.length < 5) {
Left(CommandMessages.shortName)
} else {
Right(())
}
}
validate(auth) { a =>
a.split(":") match {
case Array(uuid, key) =>
if (key.length < 64) {
Left(CommandMessages.shortKey)
} else if (!isUUID(uuid)) {
Left(CommandMessages.invalidUUID)
} else {
Right(())
}
case _ => Left(s"failed to determine authorization id and key: $a")
}
}
def isUUID(u: String) = Try(UUID.fromString(u)).isSuccess
def desiredNamespace(authKey: BasicAuthenticationAuthKey) =
Namespace(EntityName(namespace.getOrElse(subject()).trim), authKey.uuid)
}
val create = new CreateUserCmd
addSubcommand(create)
val delete = new Subcommand("delete") {
descr("delete a user")
val subject = trailArg[String](descr = "the subject to delete")
val namespace =
opt[String](descr = "delete key for given namespace only", argName = "NAMESPACE")
}
addSubcommand(delete)
val get = new Subcommand("get") {
descr("get authorization key for user")
val subject = trailArg[String](descr = "the subject to get key for")
val namespace =
opt[String](descr = "the namespace to get the key for, defaults to subject id", argName = "NAMESPACE")
val all = opt[Boolean](descr = "list all namespaces and their keys")
}
addSubcommand(get)
val whois = new Subcommand("whois") {
descr("identify user from an authorization key")
val authkey = trailArg[String](descr = "the credentials to look up 'uuid:key'")
}
addSubcommand(whois)
val list = new Subcommand("list") {
descr("list authorization keys associated with a namespace")
val namespace = trailArg[String](descr = "the namespace to lookup")
val pick = opt[Int](descr = "show no more than N identities", argName = "N", validate = _ > 0)
val key = opt[Boolean](descr = "show only the keys")
val all = opt[Boolean](descr = "show all identities")
def limit: Int = {
if (all.isSupplied) 0
else pick.getOrElse(0)
}
def showOnlyKeys = key.isSupplied
}
addSubcommand(list)
val block = new Subcommand("block") {
descr("block one or more users")
val subjects = trailArg[List[String]](descr = "one or more users to block")
}
addSubcommand(block)
val unblock = new Subcommand("unblock") {
descr("unblock one or more users")
val subjects = trailArg[List[String]](descr = "one or more users to unblock")
}
addSubcommand(unblock)
def exec(cmd: ScallopConfBase)(implicit system: ActorSystem,
logging: Logging,
transid: TransactionId): Future[Either[CommandError, String]] = {
implicit val executionContext = system.dispatcher
val authStore = UserCommand.createDataStore()
val result = cmd match {
case `create` => createUser(authStore)
case `delete` => deleteUser(authStore)
case `get` => getKey(authStore)
case `whois` => whoIs(authStore)
case `list` => list(authStore)
case `block` => changeUserState(authStore, block.subjects(), blocked = true)
case `unblock` => changeUserState(authStore, unblock.subjects(), blocked = false)
}
result.onComplete { _ =>
authStore.shutdown()
}
result
}
def createUser(authStore: AuthStore)(implicit transid: TransactionId,
ec: ExecutionContext): Future[Either[CommandError, String]] = {
val authKey = create.auth.map(BasicAuthenticationAuthKey(_)).getOrElse(BasicAuthenticationAuthKey())
authStore
.get[ExtendedAuth](DocInfo(create.subject()))
.flatMap { auth =>
val nsToUpdate = create.desiredNamespace(authKey).name
val existingNS = auth.namespaces.filter(_.namespace.name != nsToUpdate)
if (auth.isBlocked) {
Future.successful(Left(IllegalState(CommandMessages.subjectBlocked)))
} else if (!auth.namespaces.exists(_.namespace.name == nsToUpdate) || create.force.isSupplied) {
val newNS = existingNS + WhiskNamespace(create.desiredNamespace(authKey), authKey)
val newAuth = WhiskAuth(auth.subject, newNS).revision[WhiskAuth](auth.rev)
authStore.put(newAuth).map(_ => Right(authKey.compact))
} else if (create.revoke.isSupplied) {
val updatedAuthKey = auth.namespaces.find(_.namespace.name == nsToUpdate).get.authkey
val newAuthKey = new BasicAuthenticationAuthKey(updatedAuthKey.uuid, Secret())
val newNS = existingNS + WhiskNamespace(create.desiredNamespace(newAuthKey), newAuthKey)
val newAuth = WhiskAuth(auth.subject, newNS).revision[WhiskAuth](auth.rev)
authStore.put(newAuth).map(_ => Right(newAuthKey.compact))
} else {
Future.successful(Left(IllegalState(CommandMessages.namespaceExists)))
}
}
.recoverWith {
case _: NoDocumentException =>
val auth =
WhiskAuth(Subject(create.subject()), Set(WhiskNamespace(create.desiredNamespace(authKey), authKey)))
authStore.put(auth).map(_ => Right(authKey.compact))
}
}
def deleteUser(authStore: AuthStore)(implicit transid: TransactionId,
ec: ExecutionContext): Future[Either[CommandError, String]] = {
authStore
.get[ExtendedAuth](DocInfo(delete.subject()))
.flatMap { auth =>
delete.namespace
.map { namespaceToDelete =>
val newNS = auth.namespaces.filter(_.namespace.name.asString != namespaceToDelete)
if (newNS == auth.namespaces) {
Future.successful(
Left(IllegalState(CommandMessages.namespaceMissing(namespaceToDelete, delete.subject()))))
} else {
val newAuth = WhiskAuth(auth.subject, newNS).revision[WhiskAuth](auth.rev)
authStore.put(newAuth).map(_ => Right(CommandMessages.namespaceDeleted))
}
}
.getOrElse {
authStore.del(auth.docinfo).map(_ => Right(CommandMessages.subjectDeleted))
}
}
.recover {
case _: NoDocumentException =>
Left(IllegalState(CommandMessages.subjectMissing))
}
}
def getKey(authStore: AuthStore)(implicit transid: TransactionId,
ec: ExecutionContext): Future[Either[CommandError, String]] = {
authStore
.get[ExtendedAuth](DocInfo(get.subject()))
.map { auth =>
if (get.all.isSupplied) {
val msg =
auth.namespaces.map(ns => s"${ns.namespace.name}\t${ns.authkey.compact}").mkString(Properties.lineSeparator)
Right(msg)
} else {
val ns = get.namespace.getOrElse(get.subject())
auth.namespaces
.find(_.namespace.name.asString == ns)
.map(n => Right(n.authkey.compact))
.getOrElse(Left(IllegalState(CommandMessages.namespaceMissing(ns, get.subject()))))
}
} recover {
case _: NoDocumentException =>
Left(IllegalState(CommandMessages.subjectMissing))
}
}
def whoIs(authStore: AuthStore)(implicit transid: TransactionId,
ec: ExecutionContext): Future[Either[CommandError, String]] = {
Identity
.get(authStore, BasicAuthenticationAuthKey(whois.authkey()))
.map { i =>
val msg = Seq(s"subject: ${i.subject}", s"namespace: ${i.namespace}").mkString(Properties.lineSeparator)
Right(msg)
}
.recover {
case _: NoDocumentException =>
Left(IllegalState(CommandMessages.subjectMissing))
}
}
def list(authStore: AuthStore)(implicit transid: TransactionId,
ec: ExecutionContext): Future[Either[CommandError, String]] = {
Identity
.list(authStore, List(list.namespace()), limit = list.limit)
.map { rows =>
if (rows.isEmpty) Left(IllegalState(CommandMessages.namespaceMissing(list.namespace())))
else {
val msg = rows
.map { row =>
row.getFields("id", "value") match {
case Seq(JsString(subject), JsObject(value)) =>
val JsString(uuid) = value("uuid")
val JsString(secret) = value("key")
s"$uuid:$secret${if (list.showOnlyKeys) "" else s"\t$subject"}"
case _ => throw new IllegalStateException("identities view malformed")
}
}
.mkString(Properties.lineSeparator)
Right(msg)
}
}
}
def changeUserState(authStore: AuthStore, subjects: List[String], blocked: Boolean)(
implicit transid: TransactionId,
system: ActorSystem,
ec: ExecutionContext): Future[Either[CommandError, String]] = {
Source(subjects)
.mapAsync(1)(changeUserState(authStore, _, blocked))
.runWith(Sink.seq[Either[CommandError, String]])
.map { rows =>
val lefts = rows.count(_.isLeft)
val msg = rows
.map {
case Left(x) => x.message
case Right(x) => x
}
.mkString(Properties.lineSeparator)
if (lefts > 0) Left(new CommandError(msg, lefts)) else Right(msg)
}
}
private def changeUserState(authStore: AuthStore, subject: String, blocked: Boolean)(
implicit transid: TransactionId,
ec: ExecutionContext): Future[Either[CommandError, String]] = {
authStore
.get[ExtendedAuth](DocInfo(subject))
.flatMap { auth =>
val newAuth = new ExtendedAuth(auth.subject, auth.namespaces, Some(blocked))
newAuth.revision[ExtendedAuth](auth.rev)
val msg = if (blocked) CommandMessages.blocked(subject) else CommandMessages.unblocked(subject)
authStore.put(newAuth).map(_ => Right(msg))
}
.recover {
case _: NoDocumentException =>
Left(IllegalState(CommandMessages.subjectMissing(subject)))
}
}
}
object UserCommand {
def createDataStore()(implicit system: ActorSystem, logging: Logging): ArtifactStore[WhiskAuth] =
SpiLoader
.get[ArtifactStoreProvider]
.makeStore[WhiskAuth]()(classTag[WhiskAuth], ExtendedAuthFormat, WhiskDocumentReader, system, logging)
class ExtendedAuth(subject: Subject, namespaces: Set[WhiskNamespace], blocked: Option[Boolean])
extends WhiskAuth(subject, namespaces) {
override def toJson: JsObject =
blocked.map(b => JsObject(super.toJson.fields + ("blocked" -> JsBoolean(b)))).getOrElse(super.toJson)
def isBlocked: Boolean = blocked.getOrElse(false)
}
private object ExtendedAuthFormat extends RootJsonFormat[WhiskAuth] {
override def write(obj: WhiskAuth): JsValue = {
obj.toDocumentRecord
}
override def read(json: JsValue): WhiskAuth = {
val r = Try[ExtendedAuth] {
val auth = WhiskAuth.serdes.read(json)
val blocked = json.asJsObject.fields.get("blocked") match {
case Some(b: JsBoolean) => Some(b.value)
case _ => None
}
new ExtendedAuth(auth.subject, auth.namespaces, blocked).revision[ExtendedAuth](auth.rev)
}
if (r.isSuccess) r.get else throw DocumentUnreadable(Messages.corruptedEntity)
}
}
}