mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-19 00:37:23 +03:00
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:
parent
a40b4a978e
commit
32d4bf92ec
@ -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)))
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
||||
}
|
||||
|
@ -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] {
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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])
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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] =
|
||||
|
@ -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
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
Loading…
Reference in New Issue
Block a user