From 37ff9f63af753e0092e8f759bd5f299e79e58a9b Mon Sep 17 00:00:00 2001 From: Pat Losoponkul Date: Thu, 11 Jan 2024 17:11:21 +0700 Subject: [PATCH] chore: pr cleanup --- .../src/main/resources/application.conf | 5 ++-- .../oidc/KeycloakAuthenticator.scala | 29 +++++++++---------- .../oidc/KeycloakAuthenticatorImpl.scala | 13 ++------- .../authentication/oidc/KeycloakClient.scala | 2 +- .../authentication/oidc/KeycloakConfig.scala | 4 ++- .../authentication/SecurityLogicSpec.scala | 20 +++++-------- .../oidc/KeycloakAuthenticatorSpec.scala | 7 ++--- project/plugins.sbt | 1 - 8 files changed, 34 insertions(+), 47 deletions(-) diff --git a/prism-agent/service/server/src/main/resources/application.conf b/prism-agent/service/server/src/main/resources/application.conf index 7d3c7a6c41..0b890ea3ae 100644 --- a/prism-agent/service/server/src/main/resources/application.conf +++ b/prism-agent/service/server/src/main/resources/application.conf @@ -114,7 +114,7 @@ agent { autoProvisioning = ${?API_KEY_AUTO_PROVISIONING} } keycloak { - enabled = true // TODO: revert to false before merge + enabled = false enabled = ${?KEYCLOAK_ENABLED} keycloakUrl = "http://localhost:9980" @@ -135,7 +135,8 @@ agent { autoUpgradeToRPT = true autoUpgradeToRPT = ${?KEYCLOAK_UMA_AUTO_UPGRADE_RPT} - # A path of 'roles' claim in the JWT. Nested path maybe indicated by '.' separated. + # 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} } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticator.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticator.scala index c68269f6d5..7dc7597fa6 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticator.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticator.scala @@ -1,6 +1,7 @@ package io.iohk.atala.iam.authentication.oidc import io.iohk.atala.agent.walletapi.model.BaseEntity +import io.iohk.atala.agent.walletapi.model.EntityRole import io.iohk.atala.iam.authentication.AuthenticationError import io.iohk.atala.iam.authentication.AuthenticationError.AuthenticationMethodNotEnabled import io.iohk.atala.iam.authentication.AuthenticationError.InvalidCredentials @@ -14,9 +15,8 @@ import zio.* import zio.json.ast.Json import java.util.UUID -import io.iohk.atala.agent.walletapi.model.EntityRole -final class AccessToken private (token: String, claims: JwtClaim) { +final class AccessToken private (token: String, claims: JwtClaim, rolesClaimPath: Seq[String]) { override def toString(): String = token @@ -26,32 +26,30 @@ final class AccessToken private (token: String, claims: JwtClaim) { .flatMap(_.asObject.toRight("JWT payload must be a JSON object")) .map(_.contains("authorization")) - def role(claimPath: Seq[String]): Either[String, EntityRole] = { - for { - uniqueRoles <- extractRoles(claimPath).map(_.getOrElse(Nil).distinct) - r <- uniqueRoles.toList match { + def role: Either[String, EntityRole] = + extractRoles + .map(_.fold(Nil)(_.distinct).toList) + .flatMap { case Nil => Right(EntityRole.Tenant) case r :: Nil => Right(r) case _ => Left(s"Multiple roles is not supported yet.") } - } yield r - } /** Return a list of roles that is meaningful to the agent */ - private def extractRoles(claimPath: Seq[String]): Either[String, Option[Seq[EntityRole]]] = + private def extractRoles: Either[String, Option[Seq[EntityRole]]] = Json.decoder .decodeJson(claims.content) .flatMap { json => - val rolesJson = claimPath.foldLeft[Option[Json]](Some(json)) { case (acc, pathSegment) => + val rolesJson = rolesClaimPath.foldLeft(Option(json)) { case (acc, pathSegment) => acc.flatMap(_.asObject).flatMap(_.get(pathSegment)) } rolesJson match { + case None => Right(None) case Some(json) => json.asArray .toRight("Roles claim is not a JSON array of strings.") .map(_.flatMap(_.asString).flatMap(parseRole)) .map(Some(_)) - case None => Right(None) } } @@ -65,20 +63,19 @@ final class AccessToken private (token: String, claims: JwtClaim) { } object AccessToken { - def fromString(token: String): Either[String, AccessToken] = + def fromString(token: String, rolesClaimPath: Seq[String]): Either[String, AccessToken] = JwtCirce .decode(token, JwtOptions(false, false, false)) - .map(claims => AccessToken(token, claims)) + .map(claims => AccessToken(token, claims, rolesClaimPath)) .toEither .left .map(e => s"JWT token cannot be decoded. ${e.getMessage()}") } -final case class KeycloakEntity(id: UUID, accessToken: Option[AccessToken] = None, roleClaimPath: Seq[String] = Nil) - extends BaseEntity { +final case class KeycloakEntity(id: UUID, accessToken: Option[AccessToken] = None) extends BaseEntity { override def role: Either[String, EntityRole] = accessToken .toRight("Cannot extract role from KeycloakEntity without accessToken") - .flatMap(_.role(roleClaimPath)) + .flatMap(_.role) } trait KeycloakAuthenticator extends AuthenticatorWithAuthZ[KeycloakEntity] { diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorImpl.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorImpl.scala index c3d802040a..82e2fe7f2a 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorImpl.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorImpl.scala @@ -17,15 +17,13 @@ class KeycloakAuthenticatorImpl( keycloakPermissionService: PermissionManagement.Service[KeycloakEntity], ) extends KeycloakAuthenticator { - private val roleClaimPath = keycloakConfig.rolesClaimPath.split('.').toSeq - override def isEnabled: Boolean = keycloakConfig.enabled override def authenticate(token: String): IO[AuthenticationError, KeycloakEntity] = { if (isEnabled) { for { accessToken <- ZIO - .fromEither(AccessToken.fromString(token)) + .fromEither(AccessToken.fromString(token, keycloakConfig.rolesClaimPathSegments)) .mapError(AuthenticationError.InvalidCredentials.apply) introspection <- client .introspectToken(accessToken) @@ -41,17 +39,12 @@ class KeycloakAuthenticatorImpl( .attempt(UUID.fromString(id)) .mapError(e => AuthenticationError.UnexpectedError(s"Subject ID in accessToken is not a UUID. $e")) } - } yield KeycloakEntity(entityId, accessToken = Some(accessToken), roleClaimPath = roleClaimPath) + } yield KeycloakEntity(entityId, accessToken = Some(accessToken)) } else ZIO.fail(AuthenticationMethodNotEnabled("Keycloak authentication is not enabled")) } override def authorizeWalletAccessImpl(entity: KeycloakEntity): IO[AuthenticationError, WalletAccessContext] = { for { - role <- ZIO - .fromOption(entity.accessToken) - .mapError(_ => AuthenticationError.InvalidCredentials("AccessToken is missing.")) - .map(_.role(roleClaimPath).left.map(AuthenticationError.InvalidCredentials(_))) - .absolve walletId <- keycloakPermissionService .listWalletPermissions(entity) .mapError { @@ -84,7 +77,7 @@ class KeycloakAuthenticatorImpl( role <- ZIO .fromOption(entity.accessToken) .mapError(_ => AuthenticationError.InvalidCredentials("AccessToken is missing.")) - .map(_.role(roleClaimPath).left.map(AuthenticationError.InvalidCredentials(_))) + .map(_.role.left.map(AuthenticationError.InvalidCredentials(_))) .absolve ctx <- role match { case EntityRole.Admin => ZIO.succeed(WalletAdministrationContext.Admin()) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakClient.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakClient.scala index 400b053825..9b5953709b 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakClient.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakClient.scala @@ -145,7 +145,7 @@ class KeycloakClientImpl(client: AuthzClient, httpClient: Client, override val k ) .flatMap(token => ZIO - .fromEither(AccessToken.fromString(token)) + .fromEither(AccessToken.fromString(token, keycloakConfig.rolesClaimPathSegments)) .mapError(_ => KeycloakClientError.UnexpectedError("The token response was not a valid token.")) ) diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakConfig.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakConfig.scala index 23d4426edd..6612b34e2e 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakConfig.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/iam/authentication/oidc/KeycloakConfig.scala @@ -12,7 +12,9 @@ final case class KeycloakConfig( clientSecret: String, autoUpgradeToRPT: Boolean, rolesClaimPath: String, -) +) { + val rolesClaimPathSegments: Seq[String] = rolesClaimPath.split('.').toSeq +} object KeycloakConfig { val layer: URLayer[AppConfig, KeycloakConfig] = diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/SecurityLogicSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/SecurityLogicSpec.scala index 4dd378b2cf..e5bfbcfabb 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/SecurityLogicSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/SecurityLogicSpec.scala @@ -82,31 +82,27 @@ object SecurityLogicSpec extends ZIOSpecDefault { test("authorizeRole accept if the role is matched") { val tenantentity = Entity("alice", UUID.randomUUID()) val adminEntity = Entity.Admin + val tenantAuth = testAuthenticator(tenantentity) + val adminAuth = testAuthenticator(adminEntity) for { entity1 <- SecurityLogic - .authorizeRole(ApiKeyCredentials(Some(tenantentity.id.toString())))(testAuthenticator(tenantentity))( - EntityRole.Tenant - ) + .authorizeRole(ApiKeyCredentials(Some(tenantentity.id.toString())))(tenantAuth)(EntityRole.Tenant) entity2 <- SecurityLogic - .authorizeRole(ApiKeyCredentials(Some(adminEntity.id.toString())))(testAuthenticator(adminEntity))( - EntityRole.Admin - ) + .authorizeRole(ApiKeyCredentials(Some(adminEntity.id.toString())))(adminAuth)(EntityRole.Admin) } yield assert(entity1.role)(isRight(equalTo(EntityRole.Tenant))) && assert(entity2.role)(isRight(equalTo(EntityRole.Admin))) }, test("authorizeRole reject if the role is not matched") { val tenantentity = Entity("alice", UUID.randomUUID()) val adminEntity = Entity.Admin + val tenantAuth = testAuthenticator(tenantentity) + val adminAuth = testAuthenticator(adminEntity) for { exit1 <- SecurityLogic - .authorizeRole(ApiKeyCredentials(Some(tenantentity.id.toString())))(testAuthenticator(tenantentity))( - EntityRole.Admin - ) + .authorizeRole(ApiKeyCredentials(Some(tenantentity.id.toString())))(adminAuth)(EntityRole.Admin) .exit exit2 <- SecurityLogic - .authorizeRole(ApiKeyCredentials(Some(adminEntity.id.toString())))(testAuthenticator(tenantentity))( - EntityRole.Tenant - ) + .authorizeRole(ApiKeyCredentials(Some(adminEntity.id.toString())))(tenantAuth)(EntityRole.Tenant) .exit } yield assert(exit1)(fails(hasField("status", _.status, equalTo(sttp.model.StatusCode.Forbidden.code)))) && assert(exit2)(fails(hasField("status", _.status, equalTo(sttp.model.StatusCode.Forbidden.code)))) diff --git a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorSpec.scala b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorSpec.scala index d70362e0f2..658743809f 100644 --- a/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorSpec.scala +++ b/prism-agent/service/server/src/test/scala/io/iohk/atala/iam/authentication/oidc/KeycloakAuthenticatorSpec.scala @@ -262,9 +262,8 @@ object KeycloakAuthenticatorSpec entity <- authenticator.authenticate(token) role <- ZIO.fromEither(entity.role) exit <- authenticator.authorizeWalletAccess(entity).exit - } yield assert(exit)(fails(isSubtype[AuthenticationError.InvalidRole](anything))) && assert(role)( - equalTo(EntityRole.Admin) - ) + } yield assert(exit)(fails(isSubtype[AuthenticationError.InvalidRole](anything))) && + assert(role)(equalTo(EntityRole.Admin)) } ) @@ -294,7 +293,7 @@ object KeycloakAuthenticatorSpec _ <- createWalletResource(wallet.id, "wallet-1") _ <- createUser("alice", "1234") _ <- createResourcePermission(wallet.id, "alice") - token <- client.getAccessToken("alice", "1234").map(_.access_token).map(AccessToken.fromString).absolve + token <- client.getAccessToken("alice", "1234").map(_.access_token).map(AccessToken.fromString(_, Nil)).absolve rpt <- client.getRpt(token) entity <- authenticator.authenticate(rpt.toString()) permittedWallet <- authenticator.authorizeWalletAccess(entity) diff --git a/project/plugins.sbt b/project/plugins.sbt index 217e1ed483..5ccae979d2 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,7 +4,6 @@ addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0") addSbtPlugin("com.codecommit" % "sbt-github-packages" % "0.5.3") addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.11") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.6") -addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.11") addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.9") addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.6")