Do not require a JWT token for Health and Reflection services [DPP-277] (#8969)

* Do not require a JWT token for Health and Reflection services

CHANGELOG_BEGIN
- A JWT token is no longer required to call methods of Health and Reflection services
CHANGELOG_END

* Let service's authorizer decide about rejections

* Updated authorization test

* Added integration test for unsecured authorisation test for the Health service

* Added integration test for unsecured authorisation test for the Server Reflection service

* Updated Claims doc comments

* Minor change

* Reduced code duplication with SecuredServiceCallAuthTests and UnsecuredServiceCallAuthTests

* Added copyrights

* Move response status handling logic to Authorizer
This commit is contained in:
Kamil Bożek 2021-03-03 12:05:35 +01:00 committed by GitHub
parent a40b4a978e
commit 32d4bf92ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 279 additions and 183 deletions

View File

@ -15,7 +15,7 @@ import com.daml.ledger.api.auth.{
ClaimAdmin,
ClaimPublic,
ClaimReadAsParty,
Claims,
ClaimSet,
}
package object rxjava {
@ -40,19 +40,25 @@ package object rxjava {
private[rxjava] val mockedAuthService =
AuthServiceStatic {
case `emptyToken` => Claims(Nil)
case `publicToken` => Claims(Seq[Claim](ClaimPublic))
case `adminToken` => Claims(Seq[Claim](ClaimAdmin))
case `emptyToken` => ClaimSet.Unauthenticated
case `publicToken` => ClaimSet.Claims(Seq[Claim](ClaimPublic))
case `adminToken` => ClaimSet.Claims(Seq[Claim](ClaimAdmin))
case `somePartyReadToken` =>
Claims(Seq[Claim](ClaimPublic, ClaimReadAsParty(Ref.Party.assertFromString(someParty))))
ClaimSet.Claims(
Seq[Claim](ClaimPublic, ClaimReadAsParty(Ref.Party.assertFromString(someParty)))
)
case `somePartyReadWriteToken` =>
Claims(Seq[Claim](ClaimPublic, ClaimActAsParty(Ref.Party.assertFromString(someParty))))
ClaimSet.Claims(
Seq[Claim](ClaimPublic, ClaimActAsParty(Ref.Party.assertFromString(someParty)))
)
case `someOtherPartyReadToken` =>
Claims(
ClaimSet.Claims(
Seq[Claim](ClaimPublic, ClaimReadAsParty(Ref.Party.assertFromString(someOtherParty)))
)
case `someOtherPartyReadWriteToken` =>
Claims(Seq[Claim](ClaimPublic, ClaimActAsParty(Ref.Party.assertFromString(someOtherParty))))
ClaimSet.Claims(
Seq[Claim](ClaimPublic, ClaimActAsParty(Ref.Party.assertFromString(someOtherParty)))
)
}
}

View File

@ -11,7 +11,7 @@ import com.daml.auth.TokenHolder
import com.daml.bazeltools.BazelRunfiles.rlocation
import com.daml.grpc.adapter.{AkkaExecutionSequencerPool, ExecutionSequencerFactory}
import com.daml.http.util.TestUtil.requiredFile
import com.daml.ledger.api.auth.{AuthServiceStatic, Claim, ClaimPublic, Claims}
import com.daml.ledger.api.auth.{AuthServiceStatic, Claim, ClaimPublic, ClaimSet}
import com.daml.ledger.client.LedgerClient
import org.scalatest.BeforeAndAfterAll
import org.scalatest.flatspec.AsyncFlatSpec
@ -36,8 +36,8 @@ final class AuthorizationTest extends AsyncFlatSpec with BeforeAndAfterAll with
private val publicToken = "public"
private val emptyToken = "empty"
private val mockedAuthService = Option(AuthServiceStatic {
case `publicToken` => Claims(Seq[Claim](ClaimPublic))
case `emptyToken` => Claims(Nil)
case `publicToken` => ClaimSet.Claims(Seq[Claim](ClaimPublic))
case `emptyToken` => ClaimSet.Unauthenticated
})
private val accessTokenFile = Files.createTempFile("Extractor", "AuthSpec")

View File

@ -10,7 +10,7 @@ import io.grpc.Metadata
/** An interface for authorizing the ledger API access to a participant.
*
* The AuthService is responsible for converting request metadata (such as
* the HTTP headers) into a set of [[Claims]].
* the HTTP headers) into a [[ClaimSet]].
* These claims are then used by the ledger API server to check whether the
* request is authorized.
*
@ -25,15 +25,15 @@ import io.grpc.Metadata
* with a JWT token as the header value.
* - Implement `decodeMetadata()` such that it reads the JWT token
* from the corresponding HTTP header, validates the token,
* and converts the token payload to [[Claims]].
* and converts the token payload to [[ClaimSet]].
*/
trait AuthService {
/** Return empty [[Claims]] to reject requests with a UNAUTHENTICATED error status.
* Return [[Claims]] with only a single [[ClaimPublic]] claim to reject all non-public requests with a PERMISSION_DENIED status.
/** Return empty [[ClaimSet.Unauthenticated]] to reject requests with a UNAUTHENTICATED error status.
* Return [[ClaimSet.Claims]] with only a single [[ClaimPublic]] claim to reject all non-public requests with a PERMISSION_DENIED status.
* Return a failed future to reject requests with an INTERNAL error status.
*/
def decodeMetadata(headers: io.grpc.Metadata): CompletionStage[Claims]
def decodeMetadata(headers: io.grpc.Metadata): CompletionStage[ClaimSet]
/** The [[Metadata.Key]] to use for looking up the `Authorization` header in the
* request metadata.

View File

@ -22,15 +22,25 @@ class AuthServiceJWT(verifier: JwtVerifierBase) extends AuthService {
protected val logger: Logger = LoggerFactory.getLogger(AuthServiceJWT.getClass)
override def decodeMetadata(headers: Metadata): CompletionStage[Claims] = {
decodeAndParse(headers).fold(
override def decodeMetadata(headers: Metadata): CompletionStage[ClaimSet] =
CompletableFuture.completedFuture {
getAuthorizationHeader(headers) match {
case None => ClaimSet.Unauthenticated
case Some(header) => parseHeader(header)
}
}
private[this] def getAuthorizationHeader(headers: Metadata): Option[String] =
Option.apply(headers.get(AUTHORIZATION_KEY))
private[this] def parseHeader(header: String): ClaimSet =
parseJWTPayload(header).fold(
error => {
logger.warn("Authorization error: " + error.message)
CompletableFuture.completedFuture(Claims.empty)
ClaimSet.Unauthenticated
},
token => CompletableFuture.completedFuture(payloadToClaims(token)),
token => payloadToClaims(token),
)
}
private[this] def parsePayload(jwtPayload: String): Either[Error, AuthServiceJWTPayload] = {
import AuthServiceJWTCodec.JsonImplicits._
@ -39,15 +49,12 @@ class AuthServiceJWT(verifier: JwtVerifierBase) extends AuthService {
)
}
private[this] def decodeAndParse(headers: Metadata): Either[Error, AuthServiceJWTPayload] = {
val bearerTokenRegex = "Bearer (.*)".r
private[this] def parseJWTPayload(header: String): Either[Error, AuthServiceJWTPayload] = {
val BearerTokenRegex = "Bearer (.*)".r
for {
headerValue <- Option
.apply(headers.get(AUTHORIZATION_KEY))
.toRight(Error("Authorization header not found"))
token <- bearerTokenRegex
.findFirstMatchIn(headerValue)
token <- BearerTokenRegex
.findFirstMatchIn(header)
.map(_.group(1))
.toRight(Error("Authorization header does not use Bearer format"))
decoded <- verifier
@ -59,7 +66,7 @@ class AuthServiceJWT(verifier: JwtVerifierBase) extends AuthService {
} yield parsed
}
private[this] def payloadToClaims(payload: AuthServiceJWTPayload): Claims = {
private[this] def payloadToClaims(payload: AuthServiceJWTPayload): ClaimSet.Claims = {
val claims = ListBuffer[Claim]()
// Any valid token authorizes the user to use public services
@ -74,7 +81,7 @@ class AuthServiceJWT(verifier: JwtVerifierBase) extends AuthService {
payload.readAs
.foreach(party => claims.append(ClaimReadAsParty(Ref.Party.assertFromString(party))))
Claims(
ClaimSet.Claims(
claims = claims.toList,
ledgerId = payload.ledgerId,
participantId = payload.participantId,

View File

@ -7,9 +7,9 @@ import java.util.concurrent.{CompletableFuture, CompletionStage}
import io.grpc.Metadata
/** An AuthService that rejects all calls by always returning an empty set of [[Claims]] */
/** An AuthService that rejects all calls by always returning the [[ClaimSet.Unauthenticated]] */
object AuthServiceNone extends AuthService {
override def decodeMetadata(headers: Metadata): CompletionStage[Claims] = {
CompletableFuture.completedFuture(Claims.empty)
override def decodeMetadata(headers: Metadata): CompletionStage[ClaimSet] = {
CompletableFuture.completedFuture(ClaimSet.Unauthenticated)
}
}

View File

@ -8,21 +8,23 @@ import java.util.concurrent.{CompletableFuture, CompletionStage}
import io.grpc.Metadata
/** An AuthService that matches the value of the `Authorization` HTTP header against
* a static map of header values to [[Claims]].
* a static map of header values to [[ClaimSet.Claims]].
*
* Note: This AuthService is meant to be used for testing purposes only.
*/
final class AuthServiceStatic(claims: PartialFunction[String, Claims]) extends AuthService {
override def decodeMetadata(headers: Metadata): CompletionStage[Claims] = {
final class AuthServiceStatic(claims: PartialFunction[String, ClaimSet]) extends AuthService {
override def decodeMetadata(headers: Metadata): CompletionStage[ClaimSet] = {
if (headers.containsKey(AUTHORIZATION_KEY)) {
val authorizationValue = headers.get(AUTHORIZATION_KEY).stripPrefix("Bearer ")
CompletableFuture.completedFuture(claims.lift(authorizationValue).getOrElse(Claims.empty))
CompletableFuture.completedFuture(
claims.lift(authorizationValue).getOrElse(ClaimSet.Unauthenticated)
)
} else {
CompletableFuture.completedFuture(Claims.empty)
CompletableFuture.completedFuture(ClaimSet.Unauthenticated)
}
}
}
object AuthServiceStatic {
def apply(claims: PartialFunction[String, Claims]) = new AuthServiceStatic(claims)
def apply(claims: PartialFunction[String, ClaimSet]) = new AuthServiceStatic(claims)
}

View File

@ -7,9 +7,9 @@ import java.util.concurrent.{CompletableFuture, CompletionStage}
import io.grpc.Metadata
/** An AuthService that authorizes all calls by always returning a wildcard [[Claims]] */
/** An AuthService that authorizes all calls by always returning a wildcard [[ClaimSet.Claims]] */
object AuthServiceWildcard extends AuthService {
override def decodeMetadata(headers: Metadata): CompletionStage[Claims] = {
CompletableFuture.completedFuture(Claims.wildcard)
override def decodeMetadata(headers: Metadata): CompletionStage[ClaimSet] = {
CompletableFuture.completedFuture(ClaimSet.Claims.Wildcard)
}
}

View File

@ -7,12 +7,13 @@ import java.time.Instant
import com.daml.ledger.api.auth.interceptor.AuthorizationInterceptor
import com.daml.ledger.api.v1.transaction_filter.TransactionFilter
import com.daml.platform.server.api.validation.ErrorFactories.permissionDenied
import com.daml.platform.server.api.validation.ErrorFactories.{permissionDenied, unauthenticated}
import io.grpc.stub.{ServerCallStreamObserver, StreamObserver}
import org.slf4j.LoggerFactory
import scala.collection.compat._
import scala.concurrent.Future
import scala.util.{Failure, Success, Try}
/** A simple helper that allows services to use authorization claims
* that have been stored by [[AuthorizationInterceptor]].
@ -24,7 +25,7 @@ final class Authorizer(now: () => Instant, ledgerId: String, participantId: Stri
/** Validates all properties of claims that do not depend on the request,
* such as expiration time or ledger ID.
*/
private def valid(claims: Claims): Either[AuthorizationError, Unit] =
private def valid(claims: ClaimSet.Claims): Either[AuthorizationError, Unit] =
for {
_ <- claims.notExpired(now())
_ <- claims.validForLedger(ledgerId)
@ -168,7 +169,10 @@ final class Authorizer(now: () => Instant, ledgerId: String, participantId: Stri
)
}
private def ongoingAuthorization[Res](scso: ServerCallStreamObserver[Res], claims: Claims) =
private def ongoingAuthorization[Res](
scso: ServerCallStreamObserver[Res],
claims: ClaimSet.Claims,
) =
new OngoingAuthorizationObserver[Res](
scso,
claims,
@ -179,15 +183,27 @@ final class Authorizer(now: () => Instant, ledgerId: String, participantId: Stri
},
)
private def authenticatedClaimsFromContext(): Try[ClaimSet.Claims] =
AuthorizationInterceptor
.extractClaimSetFromContext()
.fold[Try[ClaimSet.Claims]](Failure(unauthenticated())) {
case ClaimSet.Unauthenticated => Failure(unauthenticated())
case claims: ClaimSet.Claims => Success(claims)
}
private def authorize[Req, Res](call: (Req, ServerCallStreamObserver[Res]) => Unit)(
authorized: Claims => Either[AuthorizationError, Unit]
authorized: ClaimSet.Claims => Either[AuthorizationError, Unit]
): (Req, StreamObserver[Res]) => Unit =
(request, observer) => {
val scso = assertServerCall(observer)
AuthorizationInterceptor
.extractClaimsFromContext()
authenticatedClaimsFromContext()
.fold(
observer.onError(_),
ex => {
logger.debug(
s"No authenticated claims found in the request context. Returning UNAUTHENTICATED"
)
observer.onError(ex)
},
claims =>
authorized(claims) match {
case Right(_) =>
@ -206,13 +222,17 @@ final class Authorizer(now: () => Instant, ledgerId: String, participantId: Stri
}
private def authorize[Req, Res](call: Req => Future[Res])(
authorized: Claims => Either[AuthorizationError, Unit]
authorized: ClaimSet.Claims => Either[AuthorizationError, Unit]
): Req => Future[Res] =
request =>
AuthorizationInterceptor
.extractClaimsFromContext()
authenticatedClaimsFromContext()
.fold(
Future.failed,
ex => {
logger.debug(
s"No authenticated claims found in the request context. Returning UNAUTHENTICATED"
)
Future.failed(ex)
},
claims =>
authorized(claims) match {
case Right(_) => call(request)

View File

@ -46,119 +46,130 @@ final case class ClaimActAsParty(name: Ref.Party) extends Claim
*/
final case class ClaimReadAsParty(name: Ref.Party) extends Claim
/** [[Claims]] define what actions an authenticated user can perform on the Ledger API.
*
* They also optionally specify an expiration epoch time that statically specifies the
* time on or after which the token will no longer be considered valid by the Ledger API.
*
* The following is a full list of services and the corresponding required claims:
* +-------------------------------------+----------------------------+------------------------------------------+
* | Ledger API service | Method | Access with |
* +-------------------------------------+----------------------------+------------------------------------------+
* | LedgerIdentityService | GetLedgerIdentity | isPublic |
* | ActiveContractsService | GetActiveContracts | for each requested party p: canReadAs(p) |
* | CommandSubmissionService | Submit | for submitting party p: canActAs(p) |
* | CommandCompletionService | CompletionEnd | isPublic |
* | CommandCompletionService | CompletionStream | for each requested party p: canReadAs(p) |
* | CommandService | * | for submitting party p: canActAs(p) |
* | LedgerConfigurationService | GetLedgerConfiguration | isPublic |
* | PackageService | * | isPublic |
* | PackageManagementService | * | isAdmin |
* | PartyManagementService | * | isAdmin |
* | ResetService | * | isAdmin |
* | TimeService | GetTime | isPublic |
* | TimeService | SetTime | isAdmin |
* | TransactionService | LedgerEnd | isPublic |
* | TransactionService | * | for each requested party p: canReadAs(p) |
* +-------------------------------------+----------------------------+------------------------------------------+
*
* @param claims List of [[Claim]]s describing the authorization this object describes.
* @param ledgerId If set, the claims will only be valid on the given ledger identifier.
* @param participantId If set, the claims will only be valid on the given participant identifier.
* @param applicationId If set, the claims will only be valid on the given application identifier.
* @param expiration If set, the claims will cease to be valid at the given time.
*/
final case class Claims(
claims: Seq[Claim],
ledgerId: Option[String] = None,
participantId: Option[String] = None,
applicationId: Option[String] = None,
expiration: Option[Instant] = None,
) {
def validForLedger(id: String): Either[AuthorizationError, Unit] =
Either.cond(ledgerId.forall(_ == id), (), AuthorizationError.InvalidLedger(ledgerId.get, id))
sealed trait ClaimSet
def validForParticipant(id: String): Either[AuthorizationError, Unit] =
Either.cond(
participantId.forall(_ == id),
(),
AuthorizationError.InvalidParticipant(participantId.get, id),
)
object ClaimSet {
object Unauthenticated extends ClaimSet
def validForApplication(id: String): Either[AuthorizationError, Unit] =
Either.cond(
applicationId.forall(_ == id),
(),
AuthorizationError.InvalidApplication(applicationId.get, id),
)
/** [[Claims]] define what actions an authenticated user can perform on the Ledger API.
*
* They also optionally specify an expiration epoch time that statically specifies the
* time on or after which the token will no longer be considered valid by the Ledger API.
*
* Please note that Health and ServerReflection services do NOT require authentication.
*
* The following is a full list of services and the corresponding required claims:
* +-------------------------------------+----------------------------+------------------------------------------+
* | Ledger API service | Method | Access with |
* +-------------------------------------+----------------------------+------------------------------------------+
* | LedgerIdentityService | GetLedgerIdentity | isPublic |
* | ActiveContractsService | GetActiveContracts | for each requested party p: canReadAs(p) |
* | CommandSubmissionService | Submit | for submitting party p: canActAs(p) |
* | CommandCompletionService | CompletionEnd | isPublic |
* | CommandCompletionService | CompletionStream | for each requested party p: canReadAs(p) |
* | CommandService | * | for submitting party p: canActAs(p) |
* | Health | * | N/A (authentication not required) |
* | LedgerConfigurationService | GetLedgerConfiguration | isPublic |
* | PackageService | * | isPublic |
* | PackageManagementService | * | isAdmin |
* | PartyManagementService | * | isAdmin |
* | ResetService | * | isAdmin |
* | ServerReflection | * | N/A (authentication not required) |
* | TimeService | GetTime | isPublic |
* | TimeService | SetTime | isAdmin |
* | TransactionService | LedgerEnd | isPublic |
* | TransactionService | * | for each requested party p: canReadAs(p) |
* +-------------------------------------+----------------------------+------------------------------------------+
*
* @param claims List of [[Claim]]s describing the authorization this object describes.
* @param ledgerId If set, the claims will only be valid on the given ledger identifier.
* @param participantId If set, the claims will only be valid on the given participant identifier.
* @param applicationId If set, the claims will only be valid on the given application identifier.
* @param expiration If set, the claims will cease to be valid at the given time.
*/
final case class Claims(
claims: Seq[Claim],
ledgerId: Option[String] = None,
participantId: Option[String] = None,
applicationId: Option[String] = None,
expiration: Option[Instant] = None,
) extends ClaimSet {
def validForLedger(id: String): Either[AuthorizationError, Unit] =
Either.cond(ledgerId.forall(_ == id), (), AuthorizationError.InvalidLedger(ledgerId.get, id))
/** Returns false if the expiration timestamp exists and is greater than or equal to the current time */
def notExpired(now: Instant): Either[AuthorizationError, Unit] =
Either.cond(
expiration.forall(now.isBefore),
(),
AuthorizationError.Expired(expiration.get, now),
)
def validForParticipant(id: String): Either[AuthorizationError, Unit] =
Either.cond(
participantId.forall(_ == id),
(),
AuthorizationError.InvalidParticipant(participantId.get, id),
)
/** Returns true if the set of claims authorizes the user to use admin services, unless the claims expired */
def isAdmin: Either[AuthorizationError, Unit] =
Either.cond(claims.contains(ClaimAdmin), (), AuthorizationError.MissingAdminClaim)
def validForApplication(id: String): Either[AuthorizationError, Unit] =
Either.cond(
applicationId.forall(_ == id),
(),
AuthorizationError.InvalidApplication(applicationId.get, id),
)
/** Returns true if the set of claims authorizes the user to use public services, unless the claims expired */
def isPublic: Either[AuthorizationError, Unit] =
Either.cond(claims.contains(ClaimPublic), (), AuthorizationError.MissingPublicClaim)
/** Returns false if the expiration timestamp exists and is greater than or equal to the current time */
def notExpired(now: Instant): Either[AuthorizationError, Unit] =
Either.cond(
expiration.forall(now.isBefore),
(),
AuthorizationError.Expired(expiration.get, now),
)
/** Returns true if the set of claims authorizes the user to act as the given party, unless the claims expired */
def canActAs(party: String): Either[AuthorizationError, Unit] = {
Either.cond(
claims.exists {
case ClaimActAsAnyParty => true
case ClaimActAsParty(p) if p == party => true
case _ => false
},
(),
AuthorizationError.MissingActClaim(party),
)
/** Returns true if the set of claims authorizes the user to use admin services, unless the claims expired */
def isAdmin: Either[AuthorizationError, Unit] =
Either.cond(claims.contains(ClaimAdmin), (), AuthorizationError.MissingAdminClaim)
/** Returns true if the set of claims authorizes the user to use public services, unless the claims expired */
def isPublic: Either[AuthorizationError, Unit] =
Either.cond(claims.contains(ClaimPublic), (), AuthorizationError.MissingPublicClaim)
/** Returns true if the set of claims authorizes the user to act as the given party, unless the claims expired */
def canActAs(party: String): Either[AuthorizationError, Unit] = {
Either.cond(
claims.exists {
case ClaimActAsAnyParty => true
case ClaimActAsParty(p) if p == party => true
case _ => false
},
(),
AuthorizationError.MissingActClaim(party),
)
}
/** Returns true if the set of claims authorizes the user to read data for the given party, unless the claims expired */
def canReadAs(party: String): Either[AuthorizationError, Unit] = {
Either.cond(
claims.exists {
case ClaimActAsAnyParty => true
case ClaimActAsParty(p) if p == party => true
case ClaimReadAsParty(p) if p == party => true
case _ => false
},
(),
AuthorizationError.MissingReadClaim(party),
)
}
}
/** Returns true if the set of claims authorizes the user to read data for the given party, unless the claims expired */
def canReadAs(party: String): Either[AuthorizationError, Unit] = {
Either.cond(
claims.exists {
case ClaimActAsAnyParty => true
case ClaimActAsParty(p) if p == party => true
case ClaimReadAsParty(p) if p == party => true
case _ => false
},
(),
AuthorizationError.MissingReadClaim(party),
object Claims {
/** A set of [[Claims]] that does not have any authorization */
val Empty: Claims = Claims(
claims = List.empty[Claim],
ledgerId = None,
participantId = None,
applicationId = None,
expiration = None,
)
/** A set of [[Claims]] that has all possible authorizations */
val Wildcard: Claims =
Empty.copy(claims = List[Claim](ClaimPublic, ClaimAdmin, ClaimActAsAnyParty))
}
}
object Claims {
/** A set of [[Claims]] that does not have any authorization */
val empty: Claims = Claims(
claims = List.empty[Claim],
ledgerId = None,
participantId = None,
applicationId = None,
expiration = None,
)
/** A set of [[Claims]] that has all possible authorizations */
val wildcard: Claims =
empty.copy(claims = List[Claim](ClaimPublic, ClaimAdmin, ClaimActAsAnyParty))
}

View File

@ -7,8 +7,8 @@ import io.grpc.stub.ServerCallStreamObserver
private[auth] final class OngoingAuthorizationObserver[A](
observer: ServerCallStreamObserver[A],
claims: Claims,
authorized: Claims => Either[AuthorizationError, Unit],
claims: ClaimSet.Claims,
authorized: ClaimSet.Claims => Either[AuthorizationError, Unit],
throwOnFailure: AuthorizationError => Throwable,
) extends ServerCallStreamObserver[A] {

View File

@ -3,8 +3,7 @@
package com.daml.ledger.api.auth.interceptor
import com.daml.ledger.api.auth.{AuthService, Claims}
import com.daml.platform.server.api.validation.ErrorFactories.unauthenticated
import com.daml.ledger.api.auth.{AuthService, ClaimSet}
import io.grpc.{
Context,
Contexts,
@ -18,7 +17,7 @@ import org.slf4j.{Logger, LoggerFactory}
import scala.compat.java8.FutureConverters
import scala.concurrent.ExecutionContext
import scala.util.{Failure, Success, Try}
import scala.util.{Failure, Success}
/** This interceptor uses the given [[AuthService]] to get [[Claims]] for the current request,
* and then stores them in the current [[Context]].
@ -30,8 +29,6 @@ final class AuthorizationInterceptor(protected val authService: AuthService, ec:
private val internalAuthenticationError =
Status.INTERNAL.withDescription("Failed to get claims from request metadata")
import AuthorizationInterceptor.contextKeyClaim
override def interceptCall[ReqT, RespT](
call: ServerCall[ReqT, RespT],
headers: Metadata,
@ -53,12 +50,8 @@ final class AuthorizationInterceptor(protected val authService: AuthService, ec:
logger.warn(s"Failed to get claims from request metadata: ${exception.getMessage}")
call.close(internalAuthenticationError, new Metadata())
new ServerCall.Listener[Nothing]() {}
case Success(Claims.empty) =>
logger.debug(s"Auth metadata decoded into empty claims, returning UNAUTHENTICATED")
call.close(Status.UNAUTHENTICATED, new Metadata())
new ServerCall.Listener[Nothing]() {}
case Success(claims) =>
val nextCtx = prevCtx.withValue(contextKeyClaim, claims)
case Success(claimSet) =>
val nextCtx = prevCtx.withValue(AuthorizationInterceptor.contextKeyClaimSet, claimSet)
// Contexts.interceptCall() creates a listener that wraps all methods of `nextListener`
// such that `Context.current` returns `nextCtx`.
val nextListenerWithContext =
@ -72,10 +65,10 @@ final class AuthorizationInterceptor(protected val authService: AuthService, ec:
object AuthorizationInterceptor {
private val contextKeyClaim = Context.key[Claims]("AuthServiceDecodedClaim")
private val contextKeyClaimSet = Context.key[ClaimSet]("AuthServiceDecodedClaim")
def extractClaimsFromContext(): Try[Claims] =
Option(contextKeyClaim.get()).fold[Try[Claims]](Failure(unauthenticated()))(Success(_))
def extractClaimSetFromContext(): Option[ClaimSet] =
Option(contextKeyClaimSet.get())
def apply(authService: AuthService, ec: ExecutionContext): AuthorizationInterceptor =
new AuthorizationInterceptor(authService, ec)

View File

@ -5,7 +5,7 @@ package com.daml.platform.sandbox.auth
import java.util.UUID
trait AdminServiceCallAuthTests extends ServiceCallAuthTests {
trait AdminServiceCallAuthTests extends SecuredServiceCallAuthTests {
private val signedIncorrectly = Option(toHeader(adminToken, UUID.randomUUID.toString))

View File

@ -12,7 +12,7 @@ import scala.concurrent.Future
* They do not test for variations in expiration time, ledger ID, or participant ID.
* It is expected that [[ReadWriteServiceCallAuthTests]] are run on the same service.
*/
trait MultiPartyServiceCallAuthTests extends ServiceCallAuthTests {
trait MultiPartyServiceCallAuthTests extends SecuredServiceCallAuthTests {
// Parties specified in the API request
sealed case class RequestSubmitters(party: String, actAs: Seq[String], readAs: Seq[String])

View File

@ -3,7 +3,7 @@
package com.daml.platform.sandbox.auth
trait PublicServiceCallAuthTests extends ServiceCallAuthTests {
trait PublicServiceCallAuthTests extends SecuredServiceCallAuthTests {
it should "deny calls with an expired read-only token" in {
expectUnauthenticated(serviceCallWithToken(canReadAsRandomPartyExpired))

View File

@ -0,0 +1,12 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.sandbox.auth
trait SecuredServiceCallAuthTests extends ServiceCallAuthTests {
behavior of serviceCallName
it should "deny unauthenticated calls" in {
expectUnauthenticated(serviceCallWithToken(None))
}
}

View File

@ -59,12 +59,6 @@ trait ServiceCallAuthTests
protected def ledgerBegin: LedgerOffset =
LedgerOffset(LedgerOffset.Value.Boundary(LedgerOffset.LedgerBoundary.LEDGER_BEGIN))
behavior of serviceCallName
it should "deny unauthenticated calls" in {
expectUnauthenticated(serviceCallWithToken(None))
}
protected val canActAsRandomParty: Option[String] =
Option(toHeader(readWriteToken(UUID.randomUUID.toString)))
protected val canActAsRandomPartyExpired: Option[String] =

View File

@ -6,7 +6,7 @@ package com.daml.platform.sandbox.auth
import java.time.Duration
import java.util.UUID
trait ServiceCallWithMainActorAuthTests extends ServiceCallAuthTests {
trait ServiceCallWithMainActorAuthTests extends SecuredServiceCallAuthTests {
protected val mainActor: String = UUID.randomUUID.toString

View File

@ -0,0 +1,12 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.sandbox.auth
trait UnsecuredServiceCallAuthTests extends ServiceCallAuthTests {
behavior of serviceCallName
it should "allow unauthenticated calls" in {
expectSuccess(serviceCallWithToken(None))
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.sandbox.auth
import com.daml.platform.testing.StreamConsumer
import io.grpc.health.v1.HealthGrpc
import io.grpc.health.v1.{HealthCheckRequest, HealthCheckResponse}
import scala.concurrent.Future
final class CheckHealthAuthIT extends UnsecuredServiceCallAuthTests {
override def serviceCallName: String = "HealthService"
private lazy val request = HealthCheckRequest.newBuilder().build()
override def serviceCallWithToken(token: Option[String]): Future[Any] =
new StreamConsumer[HealthCheckResponse](
stub(HealthGrpc.newStub(channel), token).check(request, _)
).first()
}

View File

@ -0,0 +1,19 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.platform.sandbox.auth
import com.daml.platform.testing.StreamConsumer
import io.grpc.reflection.v1alpha.{ServerReflectionGrpc, ServerReflectionResponse}
import scala.concurrent.Future
class ListServicesAuthIT extends UnsecuredServiceCallAuthTests {
override def serviceCallName: String = "ServerReflection#List"
override def serviceCallWithToken(token: Option[String]): Future[Any] =
new StreamConsumer[ServerReflectionResponse](observer =>
stub(ServerReflectionGrpc.newStub(channel), token)
.serverReflectionInfo(observer)
.onCompleted()
).first()
}