Skip to content

Commit

Permalink
feat: ATL-6832 ZIO failures and defects in entity controller (#1203)
Browse files Browse the repository at this point in the history
Signed-off-by: Benjamin Voiturier <[email protected]>
  • Loading branch information
bvoiturier authored Jun 21, 2024
1 parent 3191d8b commit 9050094
Show file tree
Hide file tree
Showing 27 changed files with 247 additions and 389 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,8 @@ object AgentInitialization {
_ <- walletService
.createWallet(defaultWallet, seed)
.orDieAsUnmanagedFailure
_ <- entityService.create(defaultEntity).mapError(e => Exception(e.message))
_ <- apiKeyAuth.add(defaultEntity.id, config.authApiKey).mapError(e => Exception(e.message))
_ <- entityService.create(defaultEntity).orDieAsUnmanagedFailure
_ <- apiKeyAuth.add(defaultEntity.id, config.authApiKey)
_ <- config.webhookUrl.fold(ZIO.unit) { url =>
val customHeaders = config.webhookApiKey.fold(Map.empty)(apiKey => Map("Authorization" -> s"Bearer $apiKey"))
walletService
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,49 @@
package org.hyperledger.identus.iam.authentication

import org.hyperledger.identus.agent.walletapi.model.{BaseEntity, Entity, EntityRole}
import org.hyperledger.identus.api.http.ErrorResponse
import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletAdministrationContext, WalletId}
import org.hyperledger.identus.shared.models.*
import zio.{IO, ZIO, ZLayer}

trait Credentials

trait AuthenticationError {
def message: String
trait AuthenticationError(
val statusCode: StatusCode,
val userFacingMessage: String
) extends Failure {
override val namespace: String = "AuthenticationError"
}

object AuthenticationError {

case class InvalidCredentials(message: String) extends AuthenticationError

case class AuthenticationMethodNotEnabled(message: String) extends AuthenticationError

case class UnexpectedError(message: String) extends AuthenticationError
case class InvalidCredentials(message: String)
extends AuthenticationError(
StatusCode.Unauthorized,
message
)

case class ServiceError(message: String) extends AuthenticationError
case class AuthenticationMethodNotEnabled(message: String)
extends AuthenticationError(
StatusCode.Unauthorized,
message
)

case class ResourceNotPermitted(message: String) extends AuthenticationError
case class UnexpectedError(message: String)
extends AuthenticationError(
StatusCode.InternalServerError,
message
)

case class InvalidRole(message: String) extends AuthenticationError
case class ResourceNotPermitted(message: String)
extends AuthenticationError(
StatusCode.Forbidden,
message
)

def toErrorResponse(error: AuthenticationError): ErrorResponse =
ErrorResponse(
status = sttp.model.StatusCode.Forbidden.code,
`type` = "authentication_error",
title = "",
detail = Option(error.message)
)
case class InvalidRole(message: String)
extends AuthenticationError(
StatusCode.Forbidden,
message
)
}

trait Authenticator[E <: BaseEntity] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import org.hyperledger.identus.iam.authentication.AuthenticationError.Authentica
import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletAdministrationContext}
import zio.*

import scala.language.implicitConversions

object SecurityLogic {

def authenticate[E <: BaseEntity](credentials: Credentials, others: Credentials*)(
Expand All @@ -31,15 +33,13 @@ object SecurityLogic {
case head :: _ => ZIO.fail(head)
}
}
.mapError(AuthenticationError.toErrorResponse)
}

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

def authorizeWalletAccess[E <: BaseEntity](credentials: Credentials, others: Credentials*)(
authenticator: Authenticator[E],
Expand All @@ -62,7 +62,6 @@ object SecurityLogic {
)(authorizer: Authorizer[E]): IO[ErrorResponse, WalletAdministrationContext] =
authorizer
.authorizeWalletAdmin(entity)
.mapError(AuthenticationError.toErrorResponse)

def authorizeWalletAdminWith[E <: BaseEntity](
credentials: (AdminApiKeyCredentials, ApiKeyCredentials, JwtCredentials)
Expand All @@ -89,11 +88,9 @@ object SecurityLogic {
.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
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package org.hyperledger.identus.iam.authentication.admin

import org.hyperledger.identus.iam.authentication.{AuthenticationError, Credentials}
import org.hyperledger.identus.shared.models.StatusCode

case class AdminApiKeyAuthenticationError(message: String) extends AuthenticationError
case class AdminApiKeyAuthenticationError(message: String)
extends AuthenticationError(
StatusCode.Unauthorized,
message
)

object AdminApiKeyAuthenticationError {
val invalidAdminApiKey = AdminApiKeyAuthenticationError("Invalid Admin API key in header `x-admin-api-key`")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import org.hyperledger.identus.iam.authentication.{
EntityAuthorizer
}
import org.hyperledger.identus.iam.authentication.AuthenticationError.*
import zio.{IO, ZIO}
import zio.{IO, UIO, ZIO}

import java.util.UUID

Expand All @@ -35,11 +35,11 @@ trait ApiKeyAuthenticator extends AuthenticatorWithAuthZ[Entity], EntityAuthoriz

def isEnabled: Boolean

def authenticate(apiKey: String): IO[AuthenticationError, Entity]
def authenticate(apiKey: String): IO[InvalidCredentials, Entity]

def add(entityId: UUID, apiKey: String): IO[AuthenticationError, Unit]
def add(entityId: UUID, apiKey: String): UIO[Unit]

def delete(entityId: UUID, apiKey: String): IO[AuthenticationError, Unit]
def delete(entityId: UUID, apiKey: String): UIO[Unit]
}

object ApiKeyAuthenticator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import org.hyperledger.identus.iam.authentication.AuthenticationError
import org.hyperledger.identus.iam.authentication.AuthenticationError.*
import org.hyperledger.identus.shared.crypto.Sha256Hash
import org.hyperledger.identus.shared.models.{WalletAdministrationContext, WalletId}
import zio.{IO, URLayer, ZIO, ZLayer}
import zio.{IO, UIO, URLayer, ZIO, ZLayer}

import java.util.UUID
import scala.language.implicitConversions
Expand All @@ -21,91 +21,70 @@ case class ApiKeyAuthenticatorImpl(

override def isEnabled: Boolean = apiKeyConfig.enabled

override def authenticate(apiKey: String): IO[AuthenticationError, Entity] = {
override def authenticate(apiKey: String): IO[InvalidCredentials, Entity] = {
if (apiKeyConfig.enabled) {
if (apiKeyConfig.authenticateAsDefaultUser) {
ZIO.succeed(Entity.Default)
} else {
authenticateBy(apiKey)
.catchSome {
case AuthenticationRepositoryError.AuthenticationNotFound(method, secret)
if apiKeyConfig.autoProvisioning =>
case InvalidCredentials(message) if apiKeyConfig.autoProvisioning =>
provisionNewEntity(apiKey)
}
.mapError {
case AuthenticationRepositoryError.AuthenticationNotFound(method, secret) =>
InvalidCredentials("Invalid API key")
case AuthenticationRepositoryError.StorageError(cause) =>
UnexpectedError("Internal error")
case AuthenticationRepositoryError.UnexpectedError(cause) =>
UnexpectedError("Internal error")
case AuthenticationRepositoryError.ServiceError(message) =>
UnexpectedError("Internal error")
case AuthenticationRepositoryError.AuthenticationCompromised(entityId, amt, secret) =>
InvalidCredentials("API key is compromised")
}
}
} else {
ZIO.fail(
AuthenticationMethodNotEnabled(s"Authentication method not enabled: ${AuthenticationMethodType.ApiKey.value}")
)
ZIO
.fail(
AuthenticationMethodNotEnabled(s"Authentication method not enabled: ${AuthenticationMethodType.ApiKey.value}")
)
.orDieAsUnmanagedFailure
}
}

protected[apikey] def provisionNewEntity(apiKey: String): IO[AuthenticationRepositoryError, Entity] = synchronized {
protected[apikey] def provisionNewEntity(apiKey: String): UIO[Entity] = synchronized {
for {
wallet <- walletManagementService
.createWallet(Wallet("Auto provisioned wallet", WalletId.random))
.orDieAsUnmanagedFailure
.provide(ZLayer.succeed(WalletAdministrationContext.Admin()))
entityToCreate = Entity(name = "Auto provisioned entity", walletId = wallet.id.toUUID)
entity <- entityService
.create(entityToCreate)
.mapError(entityServiceError => AuthenticationRepositoryError.ServiceError(entityServiceError.message))
entity <- entityService.create(entityToCreate).orDieAsUnmanagedFailure
_ <- add(entity.id, apiKey)
.mapError(are => AuthenticationRepositoryError.ServiceError(are.message))
} yield entity
}

protected[apikey] def authenticateBy(apiKey: String): IO[AuthenticationRepositoryError, Entity] = {
protected[apikey] def authenticateBy(apiKey: String): IO[InvalidCredentials, Entity] = {
for {
saltAndApiKey <- ZIO.succeed(apiKeyConfig.salt + apiKey)
secret <- ZIO
.fromTry(Try(Sha256Hash.compute(saltAndApiKey.getBytes).hexEncoded))
.logError("Failed to compute SHA256 hash")
.mapError(cause => AuthenticationRepositoryError.UnexpectedError(cause))
.orDie
entityId <- repository
.getEntityIdByMethodAndSecret(AuthenticationMethodType.ApiKey, secret)
entity <- entityService
.getById(entityId)
.mapError(entityServiceError => AuthenticationRepositoryError.ServiceError(entityServiceError.message))
.findEntityIdByMethodAndSecret(AuthenticationMethodType.ApiKey, secret)
.someOrFail(InvalidCredentials("Invalid API key"))
entity <- entityService.getById(entityId).orDieAsUnmanagedFailure
} yield entity
}

override def add(entityId: UUID, apiKey: String): IO[AuthenticationError, Unit] = {
override def add(entityId: UUID, apiKey: String): UIO[Unit] = {
for {
saltAndApiKey <- ZIO.succeed(apiKeyConfig.salt + apiKey)
secret <- ZIO
.fromTry(Try(Sha256Hash.compute(saltAndApiKey.getBytes).hexEncoded))
.logError("Failed to compute SHA256 hash")
.mapError(cause => AuthenticationError.UnexpectedError(cause.getMessage))
.orDie
_ <- repository
.insert(entityId, AuthenticationMethodType.ApiKey, secret)
.logError(s"Insert operation failed for entityId: $entityId")
.mapError(are => AuthenticationError.UnexpectedError(are.message))
.orDieAsUnmanagedFailure
} yield ()
}

override def delete(entityId: UUID, apiKey: String): IO[AuthenticationError, Unit] = {
override def delete(entityId: UUID, apiKey: String): UIO[Unit] = {
for {
saltAndApiKey <- ZIO.succeed(apiKeyConfig.salt + apiKey)
secret <- ZIO
.fromTry(Try(Sha256Hash.compute(saltAndApiKey.getBytes).hexEncoded))
.logError("Failed to compute SHA256 hash")
.mapError(cause => AuthenticationError.UnexpectedError(cause.getMessage))
_ <- repository
.delete(entityId, AuthenticationMethodType.ApiKey, secret)
.mapError(are => AuthenticationError.UnexpectedError(are.message))
.orDie
_ <- repository.delete(entityId, AuthenticationMethodType.ApiKey, secret)
} yield ()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package org.hyperledger.identus.iam.authentication.apikey
import io.getquill.*
import io.getquill.context.json.PostgresJsonExtensions
import io.getquill.doobie.DoobieContext
import org.hyperledger.identus.iam.authentication.apikey.AuthenticationRepositoryError.AuthenticationCompromised
import org.hyperledger.identus.shared.models.{Failure, StatusCode}
import zio.{IO, *}
import zio.interop.catz.*

Expand Down Expand Up @@ -34,61 +36,50 @@ trait AuthenticationRepository {
entityId: UUID,
amt: AuthenticationMethodType,
secret: String
): zio.IO[AuthenticationRepositoryError, Unit]
): zio.IO[AuthenticationCompromised, Unit]

def getEntityIdByMethodAndSecret(
def findEntityIdByMethodAndSecret(
amt: AuthenticationMethodType,
secret: String
): zio.IO[AuthenticationRepositoryError, UUID]
): zio.UIO[Option[UUID]]

def findAuthenticationMethodByTypeAndSecret(
amt: AuthenticationMethodType,
secret: String
): zio.IO[AuthenticationRepositoryError, Option[AuthenticationMethod]]
): zio.UIO[Option[AuthenticationMethod]]

def deleteByMethodAndEntityId(
entityId: UUID,
amt: AuthenticationMethodType
): zio.IO[AuthenticationRepositoryError, Unit]
): zio.UIO[Unit]

def delete(
entityId: UUID,
amt: AuthenticationMethodType,
secret: String
): zio.IO[AuthenticationRepositoryError, Unit]
): zio.UIO[Unit]
}

//TODO: reconsider the hierarchy of the service and dal layers
sealed trait AuthenticationRepositoryError {
def message: String
sealed trait AuthenticationRepositoryError(
val statusCode: StatusCode,
val userFacingMessage: String
) extends Failure {
override val namespace: String = "AuthenticationRepositoryError"
}

object AuthenticationRepositoryError {

def hide(secret: String) = secret.take(8) + "****"
case class AuthenticationNotFound(authenticationMethodType: AuthenticationMethodType, secret: String)
extends AuthenticationRepositoryError {
def message =
s"Authentication method not found for type:${authenticationMethodType.value} and secret:${hide(secret)}"
}
private def hide(secret: String) = secret.take(8) + "****"

case class AuthenticationCompromised(
entityId: UUID,
authenticationMethodType: AuthenticationMethodType,
secret: String
) extends AuthenticationRepositoryError {
def message =
s"Authentication method is compromised for entityId:$entityId, type:${authenticationMethodType.value}, and secret:${hide(secret)}"
}

case class ServiceError(message: String) extends AuthenticationRepositoryError
case class StorageError(cause: Throwable) extends AuthenticationRepositoryError {
def message = cause.getMessage
}

case class UnexpectedError(cause: Throwable) extends AuthenticationRepositoryError {
def message = cause.getMessage
}
) extends AuthenticationRepositoryError(
StatusCode.Unauthorized,
s"Authentication method is compromised for entityId:$entityId, type:${authenticationMethodType.value}, and secret:${hide(secret)}"
)
}

object AuthenticationRepositorySql extends DoobieContext.Postgres(SnakeCase) with PostgresJsonExtensions {
Expand Down
Loading

0 comments on commit 9050094

Please sign in to comment.