This is an automated email from the ASF dual-hosted git repository. markusthoemmes pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-openwhisk.git
The following commit(s) were added to refs/heads/master by this push: new e96c1bb Introduce SPI to be able to support different authentication directives. (#3829) e96c1bb is described below commit e96c1bbd5a0f54c923f0153116f7c1f165275e94 Author: Martin Henke <martin.he...@web.de> AuthorDate: Mon Jul 16 11:01:00 2018 +0200 Introduce SPI to be able to support different authentication directives. (#3829) --- ansible/group_vars/all | 2 + ansible/roles/controller/tasks/deploy.yml | 1 + common/scala/src/main/resources/reference.conf | 1 + .../whisk/core/controller/AuthenticatedRoute.scala | 29 ------------ ...te.scala => BasicAuthenticationDirective.scala} | 54 +++++++++++++--------- .../scala/whisk/core/controller/RestAPIs.scala | 47 +++++++++++-------- ...ateTests.scala => BasicAuthenticateTests.scala} | 46 +++++++++++++----- 7 files changed, 98 insertions(+), 82 deletions(-) diff --git a/ansible/group_vars/all b/ansible/group_vars/all index dc5d942..729a683 100644 --- a/ansible/group_vars/all +++ b/ansible/group_vars/all @@ -78,6 +78,8 @@ controller: seedNodes: "{{ groups['controllers'] | map('extract', hostvars, 'ansible_host') | list }}" loadbalancer: spi: "{{ controller_loadbalancer_spi | default('') }}" + authentication: + spi: "{{ controller_authentication_spi | default('') }}" loglevel: "{{ controller_loglevel | default(whisk_loglevel) | default('INFO') }}" entitlement: spi: "{{ controller_entitlement_spi | default('') }}" diff --git a/ansible/roles/controller/tasks/deploy.yml b/ansible/roles/controller/tasks/deploy.yml index e4b0885..990c18c 100644 --- a/ansible/roles/controller/tasks/deploy.yml +++ b/ansible/roles/controller/tasks/deploy.yml @@ -233,6 +233,7 @@ "{{ controller.loadbalancer.spi }}" "CONFIG_whisk_spi_EntitlementSpiProvider": "{{ controller.entitlement.spi }}" + "CONFIG_whisk_spi_AuthenticationDirectiveProvider": "{{ controller.authentication.spi }}" "CONFIG_logback_log_level": "{{ controller.loglevel }}" "CONFIG_whisk_transactions_header": "{{ transactions.header }}" diff --git a/common/scala/src/main/resources/reference.conf b/common/scala/src/main/resources/reference.conf index b658f87..2f2d898 100644 --- a/common/scala/src/main/resources/reference.conf +++ b/common/scala/src/main/resources/reference.conf @@ -9,6 +9,7 @@ whisk.spi { LogStoreProvider = whisk.core.containerpool.logging.DockerToActivationLogStoreProvider LoadBalancerProvider = whisk.core.loadBalancer.ShardingContainerPoolBalancer EntitlementSpiProvider = whisk.core.entitlement.LocalEntitlementProvider + AuthenticationDirectiveProvider = whisk.core.controller.BasicAuthenticationDirective } dispatchers { diff --git a/core/controller/src/main/scala/whisk/core/controller/AuthenticatedRoute.scala b/core/controller/src/main/scala/whisk/core/controller/AuthenticatedRoute.scala index 69a63f4..295dc89 100644 --- a/core/controller/src/main/scala/whisk/core/controller/AuthenticatedRoute.scala +++ b/core/controller/src/main/scala/whisk/core/controller/AuthenticatedRoute.scala @@ -17,40 +17,11 @@ package whisk.core.controller -import scala.concurrent.ExecutionContext -import scala.concurrent.Future - import akka.http.scaladsl.server.Route -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.model.headers.BasicHttpCredentials -import akka.http.scaladsl.server.directives._ -import akka.http.scaladsl.server.directives.AuthenticationResult -import akka.http.scaladsl.model.headers._ import whisk.common.TransactionId import whisk.core.entity.Identity -/** A common trait for secured routes */ -trait AuthenticatedRoute { - - /** An execution context for futures */ - protected implicit val executionContext: ExecutionContext - - /** Creates HTTP BasicAuth handler */ - def basicAuth[A](verify: Option[BasicHttpCredentials] => Future[Option[A]]) = { - authenticateOrRejectWithChallenge[BasicHttpCredentials, A] { creds => - verify(creds).map { - case Some(t) => AuthenticationResult.success(t) - case None => AuthenticationResult.failWithChallenge(HttpChallenges.basic("OpenWhisk secure realm")) - } - } - } - - /** Validates credentials against database of subjects */ - protected def validateCredentials(credentials: Option[BasicHttpCredentials])( - implicit transid: TransactionId): Future[Option[Identity]] -} - /** A trait for authenticated routes. */ trait AuthenticatedRouteProvider { def routes(user: Identity)(implicit transid: TransactionId): Route diff --git a/core/controller/src/main/scala/whisk/core/controller/Authenticate.scala b/core/controller/src/main/scala/whisk/core/controller/BasicAuthenticationDirective.scala similarity index 58% rename from core/controller/src/main/scala/whisk/core/controller/Authenticate.scala rename to core/controller/src/main/scala/whisk/core/controller/BasicAuthenticationDirective.scala index bcf4b21..90cf006 100644 --- a/core/controller/src/main/scala/whisk/core/controller/Authenticate.scala +++ b/core/controller/src/main/scala/whisk/core/controller/BasicAuthenticationDirective.scala @@ -17,35 +17,23 @@ package whisk.core.controller -import akka.http.scaladsl.model.headers.BasicHttpCredentials import akka.http.scaladsl.model.headers._ - -import scala.concurrent.ExecutionContext -import scala.concurrent.Future -import scala.util.Try - -import whisk.common.Logging -import whisk.common.TransactionId +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.directives.{AuthenticationDirective, AuthenticationResult} +import whisk.common.{Logging, TransactionId} import whisk.core.database.NoDocumentException +import whisk.core.entity._ import whisk.core.entity.types.AuthStore -import whisk.core.entity.BasicAuthenticationAuthKey -import whisk.core.entity.Identity -import whisk.core.entity.Secret -import whisk.core.entity.UUID -trait Authenticate { - protected implicit val executionContext: ExecutionContext - protected implicit val logging: Logging +import scala.concurrent.{ExecutionContext, Future} +import scala.util.Try - /** Database service to lookup credentials */ - protected val authStore: AuthStore +object BasicAuthenticationDirective extends AuthenticationDirectiveProvider { - /** - * Validates credentials against the authentication database; may be used in - * authentication directive. - */ - def validateCredentials(credentials: Option[BasicHttpCredentials])( - implicit transid: TransactionId): Future[Option[Identity]] = { + def validateCredentials(credentials: Option[BasicHttpCredentials])(implicit transid: TransactionId, + ec: ExecutionContext, + logging: Logging, + authStore: AuthStore): Future[Option[Identity]] = { credentials flatMap { pw => Try { // authkey deserialization is wrapped in a try to guard against malformed values @@ -72,4 +60,24 @@ trait Authenticate { Future.successful(None) } } + + /** Creates HTTP BasicAuth handler */ + def basicAuth[A](verify: Option[BasicHttpCredentials] => Future[Option[A]]): AuthenticationDirective[A] = { + extractExecutionContext.flatMap { implicit ec => + authenticateOrRejectWithChallenge[BasicHttpCredentials, A] { creds => + verify(creds).map { + case Some(t) => AuthenticationResult.success(t) + case None => AuthenticationResult.failWithChallenge(HttpChallenges.basic("OpenWhisk secure realm")) + } + } + } + } + + def authenticate(implicit transid: TransactionId, + authStore: AuthStore, + logging: Logging): AuthenticationDirective[Identity] = { + extractExecutionContext.flatMap { implicit ec => + basicAuth(validateCredentials) + } + } } diff --git a/core/controller/src/main/scala/whisk/core/controller/RestAPIs.scala b/core/controller/src/main/scala/whisk/core/controller/RestAPIs.scala index 5ed6d80..60b5eec 100644 --- a/core/controller/src/main/scala/whisk/core/controller/RestAPIs.scala +++ b/core/controller/src/main/scala/whisk/core/controller/RestAPIs.scala @@ -22,6 +22,7 @@ import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ import akka.http.scaladsl.model.StatusCodes._ import akka.http.scaladsl.model.Uri import akka.http.scaladsl.model.headers._ +import akka.http.scaladsl.server.directives.AuthenticationDirective import akka.http.scaladsl.server.{Directives, Route} import akka.stream.ActorMaterializer import pureconfig.loadConfigOrThrow @@ -37,6 +38,7 @@ import whisk.core.entity.types._ import whisk.core.loadBalancer.LoadBalancer import whisk.core.{ConfigKeys, WhiskConfig} import whisk.http.Messages +import whisk.spi.{Spi, SpiLoader} import scala.concurrent.ExecutionContext import scala.util.{Failure, Success, Try} @@ -168,13 +170,14 @@ class RestAPIVersion(config: WhiskConfig, apiPath: String, apiVersion: String)( implicit val logStore: LogStore, implicit val whiskConfig: WhiskConfig) extends SwaggerDocs(Uri.Path(apiPath) / apiVersion, "apiv1swagger.json") - with Authenticate - with AuthenticatedRoute with RespondWithHeaders { implicit val executionContext = actorSystem.dispatcher implicit val authStore = WhiskAuthStore.datastore() val whiskInfo = loadConfigOrThrow[WhiskInformation](ConfigKeys.buildInformation) + private implicit val authenticationDirectiveProvider = + SpiLoader.get[AuthenticationDirectiveProvider] + def prefix = pathPrefix(apiPath / apiVersion) /** @@ -194,33 +197,33 @@ class RestAPIVersion(config: WhiskConfig, apiPath: String, apiVersion: String)( def routes(implicit transid: TransactionId): Route = { prefix { sendCorsHeaders { - info ~ basicAuth(validateCredentials) { user => - namespaces.routes(user) ~ - pathPrefix(Collection.NAMESPACES) { - actions.routes(user) ~ - triggers.routes(user) ~ - rules.routes(user) ~ - activations.routes(user) ~ - packages.routes(user) - } - } ~ { + info ~ + authenticationDirectiveProvider.authenticate(transid, authStore, logging) { user => + namespaces.routes(user) ~ + pathPrefix(Collection.NAMESPACES) { + actions.routes(user) ~ + triggers.routes(user) ~ + rules.routes(user) ~ + activations.routes(user) ~ + packages.routes(user) + } + } ~ swaggerRoutes - } } ~ { // web actions are distinct to separate the cors header // and allow the actions themselves to respond to options - basicAuth(validateCredentials) { user => + authenticationDirectiveProvider.authenticate(transid, authStore, logging) { user => web.routes(user) } ~ { web.routes() - } ~ options { - sendCorsHeaders { - complete(OK) + } ~ + options { + sendCorsHeaders { + complete(OK) + } } - } } } - } private val namespaces = new NamespacesApi(apiPath, apiVersion) @@ -315,3 +318,9 @@ class RestAPIVersion(config: WhiskConfig, apiPath: String, apiVersion: String)( extends WhiskWebActionsApi with WhiskServices } + +trait AuthenticationDirectiveProvider extends Spi { + def authenticate(implicit transid: TransactionId, + authStore: AuthStore, + logging: Logging): AuthenticationDirective[Identity] +} diff --git a/tests/src/test/scala/whisk/core/controller/test/AuthenticateTests.scala b/tests/src/test/scala/whisk/core/controller/test/BasicAuthenticateTests.scala similarity index 73% rename from tests/src/test/scala/whisk/core/controller/test/AuthenticateTests.scala rename to tests/src/test/scala/whisk/core/controller/test/BasicAuthenticateTests.scala index 4287459..28a8e28 100644 --- a/tests/src/test/scala/whisk/core/controller/test/AuthenticateTests.scala +++ b/tests/src/test/scala/whisk/core/controller/test/BasicAuthenticateTests.scala @@ -24,7 +24,7 @@ import org.scalatest.junit.JUnitRunner import akka.http.scaladsl.model.headers.BasicHttpCredentials -import whisk.core.controller.Authenticate +import whisk.core.controller.BasicAuthenticationDirective import whisk.core.entity._ import whisk.core.entitlement.Privilege @@ -41,7 +41,7 @@ import whisk.core.entitlement.Privilege * "using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>" */ @RunWith(classOf[JUnitRunner]) -class AuthenticateTests extends ControllerTestCommon with Authenticate { +class BasicAuthenticateTests extends ControllerTestCommon { behavior of "Authenticate" it should "authorize a known user using different namespaces and cache key, and reject invalid secret" in { @@ -66,7 +66,10 @@ class AuthenticateTests extends ControllerTestCommon with Authenticate { withClue(s"Trying to login to $ns") { waitOnView(authStore, ns.authkey, 1) // wait for the view to be updated val pass = BasicHttpCredentials(ns.authkey.uuid.asString, ns.authkey.key.asString) - val user = Await.result(validateCredentials(Some(pass)), dbOpTimeout) + val user = Await.result( + BasicAuthenticationDirective + .validateCredentials(Some(pass))(transid, executionContext, logging, authStore), + dbOpTimeout) user.get shouldBe Identity(subject, ns.namespace, ns.authkey, Privilege.ALL) // first lookup should have been from datastore @@ -74,7 +77,10 @@ class AuthenticateTests extends ControllerTestCommon with Authenticate { stream.reset() // repeat query, now should be served from cache - val cachedUser = Await.result(validateCredentials(Some(pass))(transid()), dbOpTimeout) + val cachedUser = Await.result( + BasicAuthenticationDirective + .validateCredentials(Some(pass))(transid, executionContext, logging, authStore), + dbOpTimeout) cachedUser.get shouldBe Identity(subject, ns.namespace, ns.authkey, Privilege.ALL) stream.toString should include(s"serving from cache: ${CacheKey(ns.authkey)}") @@ -87,7 +93,10 @@ class AuthenticateTests extends ControllerTestCommon with Authenticate { val key = ns.authkey.key.asString Seq(key.drop(1), key.dropRight(1), key + "x", BasicAuthenticationAuthKey().key.asString).foreach { k => val pass = BasicHttpCredentials(ns.authkey.uuid.asString, k) - val user = Await.result(validateCredentials(Some(pass)), dbOpTimeout) + val user = Await.result( + BasicAuthenticationDirective + .validateCredentials(Some(pass))(transid, executionContext, logging, authStore), + dbOpTimeout) user shouldBe empty } } @@ -96,7 +105,9 @@ class AuthenticateTests extends ControllerTestCommon with Authenticate { implicit val tid = transid() val creds = WhiskAuthHelpers.newIdentity() val pass = creds.authkey.getCredentials.asInstanceOf[Option[BasicHttpCredentials]] - val user = Await.result(validateCredentials(pass), dbOpTimeout) + val user = Await.result( + BasicAuthenticationDirective.validateCredentials(pass)(transid, executionContext, logging, authStore), + dbOpTimeout) user should be(None) stream.toString should not include pass.get.password } @@ -105,34 +116,47 @@ class AuthenticateTests extends ControllerTestCommon with Authenticate { implicit val tid = transid() val creds = WhiskAuthHelpers.newIdentity() val pass = creds.authkey.getCredentials.asInstanceOf[Option[BasicHttpCredentials]] - val user = Await.result(validateCredentials(pass), dbOpTimeout) + val user = Await.result( + BasicAuthenticationDirective.validateCredentials(pass)(transid, executionContext, logging, authStore), + dbOpTimeout) user should be(None) } it should "not authorize when no user creds are provided" in { implicit val tid = transid() - val user = Await.result(validateCredentials(None), dbOpTimeout) + val user = Await.result( + BasicAuthenticationDirective.validateCredentials(None)(transid, executionContext, logging, authStore), + dbOpTimeout) user should be(None) } it should "not authorize when malformed user is provided" in { implicit val tid = transid() val pass = BasicHttpCredentials("x", Secret().asString) - val user = Await.result(validateCredentials(Some(pass)), dbOpTimeout) + val user = Await.result( + BasicAuthenticationDirective + .validateCredentials(Some(pass))(transid, executionContext, logging, authStore), + dbOpTimeout) user should be(None) } it should "not authorize when malformed secret is provided" in { implicit val tid = transid() val pass = BasicHttpCredentials(UUID().asString, "x") - val user = Await.result(validateCredentials(Some(pass)), dbOpTimeout) + val user = Await.result( + BasicAuthenticationDirective + .validateCredentials(Some(pass))(transid, executionContext, logging, authStore), + dbOpTimeout) user should be(None) } it should "not authorize when malformed creds are provided" in { implicit val tid = transid() val pass = BasicHttpCredentials("x", "y") - val user = Await.result(validateCredentials(Some(pass)), dbOpTimeout) + val user = Await.result( + BasicAuthenticationDirective + .validateCredentials(Some(pass))(transid, executionContext, logging, authStore), + dbOpTimeout) user should be(None) } }