Skip to content

Commit

Permalink
feat(prism-agent): add JWT auth support for agent-admin role (#840)
Browse files Browse the repository at this point in the history
Signed-off-by: Pat Losoponkul <pat.losoponkul@iohk.io>
  • Loading branch information
patlo-iog committed Jan 12, 2024
1 parent b48fe3c commit 3ccd56e
Show file tree
Hide file tree
Showing 37 changed files with 449 additions and 194 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Example JWT payload containing `ClientRole`. (Some claims are omitted for readab
"resource_access": {
"prism-agent": {
"roles": [
"agent-admin"
"admin"
]
},
"account": {
Expand Down Expand Up @@ -107,9 +107,9 @@ __Proposed agent role authorization model__
Role is a plain text that defines what level of access a user has on a system.
For the agent, it needs to support 2 roles:

1. __Admin__: `agent-admin`. Admin can never access a tenant wallet.
1. __Admin__: `admin`. Admin can never access a tenant wallet.
Agent auth layer must ignore any UMA permission to the wallet.
2. __Tenant__: `agent-tenant` or implicitly inferred if another role is not specified.
2. __Tenant__: `tenant` or implicitly inferred if another role is not specified.
Tenant must have UMA permission defined to access the wallet.

### Positive Consequences
Expand Down
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: [admin, 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 authorizeWalletAccessLogic(entity: E): IO[AuthenticationError, WalletAccessContext]

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(_ => authorizeWalletAccessLogic(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 authorizeWalletAccessLogic(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 authorizeWalletAccessLogic(entity: BaseEntity): IO[AuthenticationError, WalletAccessContext] =
EntityAuthorizer.authorizeWalletAccessLogic(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 authorizeWalletAccessLogic(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)
} 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

0 comments on commit 3ccd56e

Please sign in to comment.