Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(prism-agent): add JWT auth support for agent-admin role #840

Merged
merged 23 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ agent {
# if disabled, accessToken must be RPT which already include the permission claims.
autoUpgradeToRPT = true
autoUpgradeToRPT = ${?KEYCLOAK_UMA_AUTO_UPGRADE_RPT}

# A path of 'roles' claim in the JWT. Nested path maybe indicated by '.' separator.
# The JWT 'roles' claim is expected to be a list of the following values: [agent-admin, agent-tenant]
rolesClaimPath = "resource_access."${agent.authentication.keycloak.clientId}".roles"
rolesClaimPath = ${?KEYKLOAK_ROLES_CLAIM_PATH}
}
}
database {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class DIDRegistrarServerEndpoints(

private val listManagedDidServerEndpoint: ZServerEndpoint[Any, Any] =
DIDRegistrarEndpoints.listManagedDid
.zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer))
.zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer))
.serverLogic { wac =>
{ case (rc, paginationInput) =>
didRegistrarController
Expand All @@ -28,7 +28,7 @@ class DIDRegistrarServerEndpoints(

private val createManagedDidServerEndpoint: ZServerEndpoint[Any, Any] =
DIDRegistrarEndpoints.createManagedDid
.zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer))
.zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer))
.serverLogic { wac =>
{ case (rc, createManagedDidRequest) =>
didRegistrarController
Expand All @@ -39,7 +39,7 @@ class DIDRegistrarServerEndpoints(

private val getManagedDidServerEndpoint: ZServerEndpoint[Any, Any] =
DIDRegistrarEndpoints.getManagedDid
.zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer))
.zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer))
.serverLogic { wac =>
{ case (rc, did) =>
didRegistrarController
Expand All @@ -50,7 +50,7 @@ class DIDRegistrarServerEndpoints(

private val publishManagedDidServerEndpoint: ZServerEndpoint[Any, Any] =
DIDRegistrarEndpoints.publishManagedDid
.zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer))
.zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer))
.serverLogic { wac =>
{ case (rc, did) =>
didRegistrarController
Expand All @@ -61,7 +61,7 @@ class DIDRegistrarServerEndpoints(

private val updateManagedDidServerEndpoint: ZServerEndpoint[Any, Any] =
DIDRegistrarEndpoints.updateManagedDid
.zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer))
.zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer))
.serverLogic { wac =>
{ case (rc, did, updateRequest) =>
didRegistrarController
Expand All @@ -72,7 +72,7 @@ class DIDRegistrarServerEndpoints(

private val deactivateManagedDidServerEndpoint: ZServerEndpoint[Any, Any] =
DIDRegistrarEndpoints.deactivateManagedDid
.zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer))
.zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer))
.serverLogic { wac =>
{ case (rc, did) =>
didRegistrarController
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class ConnectionServerEndpoints(

private val createConnectionServerEndpoint: ZServerEndpoint[Any, Any] =
createConnection
.zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer))
.zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer))
.serverLogic { wac =>
{ case (ctx: RequestContext, request: CreateConnectionRequest) =>
connectionController
Expand All @@ -33,7 +33,7 @@ class ConnectionServerEndpoints(

private val getConnectionServerEndpoint: ZServerEndpoint[Any, Any] =
getConnection
.zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer))
.zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer))
.serverLogic { wac =>
{ case (ctx: RequestContext, connectionId: UUID) =>
connectionController
Expand All @@ -44,7 +44,7 @@ class ConnectionServerEndpoints(

private val getConnectionsServerEndpoint: ZServerEndpoint[Any, Any] =
getConnections
.zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer))
.zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer))
.serverLogic { wac =>
{ case (ctx: RequestContext, paginationInput: PaginationInput, thid: Option[String]) =>
connectionController
Expand All @@ -55,7 +55,7 @@ class ConnectionServerEndpoints(

private val acceptConnectionInvitationServerEndpoint: ZServerEndpoint[Any, Any] =
acceptConnectionInvitation
.zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer))
.zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer))
.serverLogic { wac =>
{ case (ctx: RequestContext, request: AcceptConnectionInvitationRequest) =>
connectionController
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class EventServerEndpoints(

val createWebhookNotificationServerEndpoint: ZServerEndpoint[Any, Any] =
EventEndpoints.createWebhookNotification
.zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer))
.zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer))
.serverLogic { wac =>
{ case (rc, createWebhook) =>
eventController
Expand All @@ -28,7 +28,7 @@ class EventServerEndpoints(

val listWebhookNotificationServerEndpoint: ZServerEndpoint[Any, Any] =
EventEndpoints.listWebhookNotification
.zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer))
.zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer))
.serverLogic { wac => rc =>
eventController
.listWebhookNotifications(rc)
Expand All @@ -37,7 +37,7 @@ class EventServerEndpoints(

val deleteWebhookNotificationServerEndpoint: ZServerEndpoint[Any, Any] =
EventEndpoints.deleteWebhookNotification
.zServerSecurityLogic(SecurityLogic.authorizeWith(_)(authenticator, authorizer))
.zServerSecurityLogic(SecurityLogic.authorizeWalletAccessWith(_)(authenticator, authorizer))
.serverLogic { wac =>
{ case (rc, id) =>
eventController
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.iohk.atala.iam.authentication

import io.iohk.atala.agent.walletapi.model.BaseEntity
import io.iohk.atala.agent.walletapi.model.Entity
import io.iohk.atala.agent.walletapi.model.EntityRole
import io.iohk.atala.api.http.ErrorResponse
import io.iohk.atala.shared.models.WalletAccessContext
import io.iohk.atala.shared.models.WalletAdministrationContext
Expand All @@ -26,6 +27,8 @@ object AuthenticationError {

case class ResourceNotPermitted(message: String) extends AuthenticationError

case class InvalidRole(message: String) extends AuthenticationError

def toErrorResponse(error: AuthenticationError): ErrorResponse =
ErrorResponse(
status = sttp.model.StatusCode.Forbidden.code,
Expand All @@ -45,14 +48,26 @@ trait Authenticator[E <: BaseEntity] {
}

trait Authorizer[E <: BaseEntity] {
def authorize(entity: E): IO[AuthenticationError, WalletAccessContext]
protected def authorizeWalletAccessImpl(entity: E): IO[AuthenticationError, WalletAccessContext]
patlo-iog marked this conversation as resolved.
Show resolved Hide resolved

final def authorizeWalletAccess(entity: E): IO[AuthenticationError, WalletAccessContext] =
ZIO
.fromEither(entity.role)
.mapError(msg =>
AuthenticationError.UnexpectedError(s"Unable to retrieve entity role for entity id ${entity.id}. $msg")
)
.filterOrFail(_ != EntityRole.Admin)(
AuthenticationError.InvalidRole("Admin role is not allowed to access the tenant's wallet.")
)
.flatMap(_ => authorizeWalletAccessImpl(entity))

def authorizeWalletAdmin(entity: E): IO[AuthenticationError, WalletAdministrationContext]
}

object EntityAuthorizer extends EntityAuthorizer

trait EntityAuthorizer extends Authorizer[Entity] {
override def authorize(entity: Entity): IO[AuthenticationError, WalletAccessContext] =
override def authorizeWalletAccessImpl(entity: Entity): IO[AuthenticationError, WalletAccessContext] =
ZIO.succeed(entity.walletId).map(WalletId.fromUUID).map(WalletAccessContext.apply)

override def authorizeWalletAdmin(entity: Entity): IO[AuthenticationError, WalletAdministrationContext] = {
Expand All @@ -69,8 +84,8 @@ object DefaultEntityAuthenticator extends AuthenticatorWithAuthZ[BaseEntity] {

override def isEnabled: Boolean = true
override def authenticate(credentials: Credentials): IO[AuthenticationError, BaseEntity] = ZIO.succeed(Entity.Default)
override def authorize(entity: BaseEntity): IO[AuthenticationError, WalletAccessContext] =
EntityAuthorizer.authorize(Entity.Default)
override def authorizeWalletAccessImpl(entity: BaseEntity): IO[AuthenticationError, WalletAccessContext] =
EntityAuthorizer.authorizeWalletAccessImpl(Entity.Default)
override def authorizeWalletAdmin(entity: BaseEntity): IO[AuthenticationError, WalletAdministrationContext] =
EntityAuthorizer.authorizeWalletAdmin(Entity.Default)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ case class DefaultAuthenticator(
case keycloakCredentials: JwtCredentials => keycloakAuthenticator(keycloakCredentials)
}

override def authorize(entity: BaseEntity): IO[AuthenticationError, WalletAccessContext] = entity match {
case entity: Entity => EntityAuthorizer.authorize(entity)
case kcEntity: KeycloakEntity => keycloakAuthenticator.authorize(kcEntity)
}
override def authorizeWalletAccessImpl(entity: BaseEntity): IO[AuthenticationError, WalletAccessContext] =
entity match {
case entity: Entity => EntityAuthorizer.authorizeWalletAccess(entity)
case kcEntity: KeycloakEntity => keycloakAuthenticator.authorizeWalletAccess(kcEntity)
}

override def authorizeWalletAdmin(entity: BaseEntity): IO[AuthenticationError, WalletAdministrationContext] =
entity match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.iohk.atala.iam.authentication

import io.iohk.atala.agent.walletapi.model.BaseEntity
import io.iohk.atala.agent.walletapi.model.Entity
import io.iohk.atala.agent.walletapi.model.EntityRole
import io.iohk.atala.api.http.ErrorResponse
import io.iohk.atala.iam.authentication.AuthenticationError.AuthenticationMethodNotEnabled
import io.iohk.atala.iam.authentication.admin.AdminApiKeyCredentials
Expand Down Expand Up @@ -36,26 +37,28 @@ object SecurityLogic {
.mapError(AuthenticationError.toErrorResponse)
}

def authorize[E <: BaseEntity](entity: E)(authorizer: Authorizer[E]): IO[ErrorResponse, WalletAccessContext] =
def authorizeWalletAccess[E <: BaseEntity](
entity: E
)(authorizer: Authorizer[E]): IO[ErrorResponse, WalletAccessContext] =
authorizer
.authorize(entity)
.authorizeWalletAccess(entity)
.mapError(AuthenticationError.toErrorResponse)

def authorize[E <: BaseEntity](credentials: Credentials, others: Credentials*)(
def authorizeWalletAccess[E <: BaseEntity](credentials: Credentials, others: Credentials*)(
authenticator: Authenticator[E],
authorizer: Authorizer[E]
): IO[ErrorResponse, WalletAccessContext] =
authenticate[E](credentials, others: _*)(authenticator)
.flatMap {
case Left(entity) => authorize(entity)(EntityAuthorizer)
case Right(entity) => authorize(entity)(authorizer)
case Left(entity) => authorizeWalletAccess(entity)(EntityAuthorizer)
case Right(entity) => authorizeWalletAccess(entity)(authorizer)
}

def authorizeWith[E <: BaseEntity](credentials: (ApiKeyCredentials, JwtCredentials))(
def authorizeWalletAccessWith[E <: BaseEntity](credentials: (ApiKeyCredentials, JwtCredentials))(
authenticator: Authenticator[E],
authorizer: Authorizer[E]
): IO[ErrorResponse, WalletAccessContext] =
authorize[E](credentials._2, credentials._1)(authenticator, authorizer)
authorizeWalletAccess[E](credentials._2, credentials._1)(authenticator, authorizer)

def authorizeWalletAdmin[E <: BaseEntity](
entity: E
Expand All @@ -75,4 +78,32 @@ object SecurityLogic {
case Left(entity) => authorizeWalletAdmin(entity)(EntityAuthorizer).map(entity -> _)
case Right(entity) => authorizeWalletAdmin(entity)(authorizer).map(entity -> _)
}

def authorizeRole[E <: BaseEntity](credentials: Credentials, others: Credentials*)(
authenticator: Authenticator[E],
)(permittedRole: EntityRole): IO[ErrorResponse, BaseEntity] = {
authenticate[E](credentials, others: _*)(authenticator)
.flatMap { ee =>
val entity = ee.fold(identity, identity)
for {
role <-
ZIO
.fromEither(entity.role)
.mapError(msg =>
AuthenticationError.UnexpectedError(s"Unable to retrieve entity role for entity id ${entity.id}. $msg")
)
.mapError(AuthenticationError.toErrorResponse)
_ <- ZIO
.fail(AuthenticationError.InvalidRole(s"$role role is not permitted. Expected $permittedRole role."))
.when(role != permittedRole)
.mapError(AuthenticationError.toErrorResponse)
patlo-iog marked this conversation as resolved.
Show resolved Hide resolved
} yield entity
}
}

def authorizeRoleWith[E <: BaseEntity](credentials: (AdminApiKeyCredentials, JwtCredentials))(
authenticator: Authenticator[E],
)(permittedRole: EntityRole): IO[ErrorResponse, BaseEntity] =
authorizeRole(credentials._1, credentials._2)(authenticator)(permittedRole)

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ trait AdminApiKeyAuthenticator extends AuthenticatorWithAuthZ[Entity], EntityAut
credentials match {
case AdminApiKeyCredentials(Some(apiKey)) => authenticate(apiKey)
case AdminApiKeyCredentials(None) =>
ZIO.logInfo(s"AdminApiKey API authentication is enabled, but `x-admin-api-key` token is empty") *>
ZIO.logDebug(s"AdminApiKey API authentication is enabled, but `x-admin-api-key` token is empty") *>
ZIO.fail(AdminApiKeyAuthenticationError.emptyAdminApiKey)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ case class AdminApiKeyAuthenticatorImpl(adminConfig: AdminConfig) extends AdminA
def authenticate(adminApiKey: String): IO[AuthenticationError, Entity] = {
if (adminApiKey == adminConfig.token) {
ZIO.logDebug(s"Admin API key authentication successful") *>
ZIO.succeed(Admin)
ZIO.succeed(Entity.Admin)
} else ZIO.fail(AdminApiKeyAuthenticationError.invalidAdminApiKey)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ case class AdminApiKeyAuthenticationError(message: String) extends Authenticatio

object AdminApiKeyAuthenticationError {
val invalidAdminApiKey = AdminApiKeyAuthenticationError("Invalid Admin API key in header `x-admin-api-key`")
val emptyAdminApiKey = AdminApiKeyAuthenticationError("Empty `x-admin-apikey` header provided")
val emptyAdminApiKey = AdminApiKeyAuthenticationError("Empty `x-admin-api-key` header provided")
}

case class AdminApiKeyCredentials(apiKey: Option[String]) extends Credentials
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package io.iohk.atala.iam.authentication.admin

import io.iohk.atala.agent.walletapi.model.BaseEntity
import io.iohk.atala.api.http.ErrorResponse
import io.iohk.atala.iam.authentication.{AuthenticationError, Authenticator}
import sttp.tapir.EndpointIO
import sttp.tapir.EndpointInput.Auth
import sttp.tapir.EndpointInput.AuthType.ApiKey
import sttp.tapir.ztapir.*
import zio.*

object AdminApiKeySecurityLogic {

Expand All @@ -19,12 +15,4 @@ object AdminApiKeySecurityLogic {
)
.securitySchemeName("adminApiKeyAuth")

def securityLogic[E <: BaseEntity](
credentials: AdminApiKeyCredentials
)(authenticator: Authenticator[E]): IO[ErrorResponse, E] =
ZIO
.succeed(authenticator)
.flatMap(_.authenticate(credentials))
.mapError(error => AuthenticationError.toErrorResponse(error))

}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ trait ApiKeyAuthenticator extends AuthenticatorWithAuthZ[Entity], EntityAuthoriz
apiKey match {
case Some(value) if value.nonEmpty => authenticate(value)
case Some(value) =>
ZIO.logInfo(s"ApiKey API authentication is enabled, but `apikey` token is empty") *>
ZIO.logDebug(s"ApiKey API authentication is enabled, but `apikey` token is empty") *>
ZIO.fail(ApiKeyAuthenticationError.emptyApiKey)
case None =>
ZIO.logInfo(s"ApiKey API authentication is enabled, but `apikey` token is not provided") *>
ZIO.logDebug(s"ApiKey API authentication is enabled, but `apikey` token is not provided") *>
ZIO.fail(InvalidCredentials("ApiKey key is not provided"))
}
case other =>
Expand Down
Loading
Loading