Skip to content

Commit

Permalink
chore: pr cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
Pat Losoponkul committed Jan 11, 2024
1 parent 2245e9c commit 37ff9f6
Show file tree
Hide file tree
Showing 8 changed files with 34 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand All @@ -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)
}
}

Expand All @@ -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] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."))
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
)

Expand Down Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down

0 comments on commit 37ff9f6

Please sign in to comment.