Skip to content

Commit

Permalink
feat(prism-agent): implement the logic to grant and revoke permission…
Browse files Browse the repository at this point in the history
…s for the user to have access to the wallet

Signed-off-by: Yurii Shynbuiev <yurii.shynbuiev@iohk.io>
  • Loading branch information
yshyn-iohk committed Nov 3, 2023
1 parent 6d8d05b commit 91228ae
Show file tree
Hide file tree
Showing 10 changed files with 462 additions and 38 deletions.
12 changes: 10 additions & 2 deletions infrastructure/shared/docker-compose-demo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
version: "3.8"

services:

db:
image: postgres:13
environment:
Expand Down Expand Up @@ -53,7 +52,16 @@ services:
prism-node:
condition: service_started
healthcheck:
test: ["CMD", "wget", "--timeout=5", "--tries=3", "-O", "/dev/null", "http://prism-agent:8085/_system/health"]
test:
[
"CMD",
"wget",
"--timeout=5",
"--tries=3",
"-O",
"/dev/null",
"http://prism-agent:8085/_system/health",
]
interval: 30s
timeout: 10s
retries: 5
Expand Down
11 changes: 10 additions & 1 deletion infrastructure/shared/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,16 @@ services:
vault-server:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--timeout=5", "--tries=3", "-O", "/dev/null", "http://prism-agent:8085/_system/health"]
test:
[
"CMD",
"wget",
"--timeout=5",
"--tries=3",
"-O",
"/dev/null",
"http://prism-agent:8085/_system/health",
]
interval: 30s
timeout: 10s
retries: 5
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.iohk.atala.iam.authorization.core

import io.iohk.atala.shared.models.WalletId
import zio.IO

import java.util.UUID

object PermissionManagement {
trait Service {
def grantWalletToUser(walletId: WalletId, userId: UUID): IO[Error, Unit]
def revokeWalletFromUser(walletId: WalletId, userId: UUID): IO[Error, Unit]
}

trait Error(message: String)

object Error {
case class UserNotFoundById(userId: UUID, cause: Option[Throwable] = None)
extends Error(s"User $userId is not found" + cause.map(t => s" Cause: ${t.getMessage}"))
case class WalletNotFoundByUserId(userId: UUID) extends Error(s"Wallet for user $userId is not found")

case class WalletNotFoundById(walletId: WalletId) extends Error(s"Wallet not found by ${walletId.toUUID}")

case class WalletResourceNotFoundById(walletId: WalletId)
extends Error(s"Wallet resource not found by ${walletId.toUUID}")

case class PermissionNotFoundById(userId: UUID, walletId: WalletId, walletResourceId: String)
extends Error(
s"Permission not found by userId: $userId, walletId: ${walletId.toUUID}, walletResourceId: $walletResourceId"
)

case class UnexpectedError(cause: Throwable) extends Error(cause.getMessage)

case class ServiceError(message: String) extends Error(message)
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,163 @@
package io.iohk.atala.iam.authorization.keycloak.admin

import io.iohk.atala.agent.walletapi.service.WalletManagementService
import io.iohk.atala.iam.authorization.core.PermissionManagement
import io.iohk.atala.iam.authorization.core.PermissionManagement.Error.*
import io.iohk.atala.shared.models.WalletId
import org.keycloak.admin.client.Keycloak
import zio.IO
import org.keycloak.authorization.client.AuthzClient
import org.keycloak.representations.idm.authorization.{ResourceRepresentation, UmaPermissionRepresentation}
import zio.ZIO.*
import zio.ZLayer.*
import zio.{IO, Task, URLayer, ZIO, ZLayer}

import java.util.UUID
import scala.jdk.CollectionConverters.*

class KeycloakPermissionManagementService(keycloak: Keycloak) extends PermissionManagement.Service {
override def grantWalletToUser(walletId: WalletId, userId: UUID): IO[PermissionManagement.Error, Unit] = ???
case class KeycloakPermissionManagementService(
authzClient: AuthzClient,
walletManagementService: WalletManagementService
) extends PermissionManagement.Service {

override def revokeWalletFromUser(walletId: WalletId, userId: UUID): IO[PermissionManagement.Error, Unit] = ???
private def walletResourceName(walletId: WalletId) = s"wallet-${walletId.toUUID.toString}"

override def getWalletForUser(userId: UUID): IO[PermissionManagement.Error, WalletId] = ???
private def policyName(userId: String, resourceId: String) = s"user $userId on wallet $resourceId permission"

override def grantWalletToUser(walletId: WalletId, userId: UUID): IO[PermissionManagement.Error, Unit] = {
for {
walletOpt <- walletManagementService
.getWallet(walletId)
.mapError(wmse => ServiceError(wmse.toThrowable.getMessage))

wallet <- ZIO
.fromOption(walletOpt)
.orElseFail(WalletNotFoundById(walletId))

walletResourceOpt <- findWalletResource(walletId)
.logError("Error while finding wallet resource")
.mapError(UnexpectedError.apply)

walletResource <- ZIO
.fromOption(walletResourceOpt)
.orElse(createWalletResource(walletId))
.logError("Error while creating wallet resource")
.mapError(UnexpectedError.apply)
_ <- ZIO.log(s"Wallet resource created ${walletResource.toString}")

permission <- createResourcePermission(walletResource.getId, userId.toString)
.mapError(UnexpectedError.apply)

_ <- ZIO.log(s"Permission created with id ${permission.getId} and name ${permission.getName}")
} yield ()
}

private def permissionDetails(permission: UmaPermissionRepresentation): String = {
s"""
|id: ${permission.getId}
|name: ${permission.getName}
|scopes: ${permission.getScopes.asScala.mkString(", ")}
|users: ${permission.getUsers.asScala.mkString(", ")}
|""".stripMargin
}

private def createResourcePermission(resourceId: String, userId: String): Task[UmaPermissionRepresentation] = {
val policy = UmaPermissionRepresentation()
policy.setName(policyName(userId, resourceId))
policy.setUsers(Set(userId).asJava)

for {
umaPermissionRepresentation <- ZIO.attemptBlocking(
authzClient
.protection()
.policy(resourceId)
.create(policy)
)
} yield umaPermissionRepresentation
}

private def findWalletResource(walletId: WalletId): Task[Option[ResourceRepresentation]] = {
for {
walletResourceOrNull <- ZIO.attemptBlocking(
authzClient.protection().resource().findByName(walletResourceName(walletId))
)
} yield Option(walletResourceOrNull)
}

private def createWalletResource(walletId: WalletId): Task[ResourceRepresentation] = {
val walletResource = ResourceRepresentation()
walletResource.setId(walletId.toUUID.toString)
walletResource.setUris(Set(s"/wallets/${walletResourceName(walletId)}").asJava)
walletResource.setName(walletResourceName(walletId))
walletResource.setOwnerManagedAccess(true)

for {
_ <- ZIO.log(s"Creating resource for the wallet ${walletId.toUUID.toString}")
response <- ZIO.attemptBlocking(
authzClient
.protection()
.resource()
.create(walletResource)
)
resource <- ZIO.attemptBlocking(
authzClient
.protection()
.resource()
.findById(walletResource.getId)
)
_ <- ZIO.log(s"Resource for the wallet created id: ${resource.getId}, name ${resource.getName}")
} yield resource
}

override def revokeWalletFromUser(walletId: WalletId, userId: UUID): IO[PermissionManagement.Error, Unit] = {
for {
walletResourceOpt <- findWalletResource(walletId)
.logError("Error while finding wallet resource")
.mapError(UnexpectedError.apply)

walletResource <- ZIO
.fromOption(walletResourceOpt)
.orElseFail(WalletResourceNotFoundById(walletId))

permissionOpt <- ZIO
.attemptBlocking(
authzClient
.protection()
.policy(walletResource.getId)
.find(
policyName(userId.toString, walletResource.getId),
null,
0,
1
)
)
.map(_.asScala.headOption)
.logError(s"Error while finding permission by name ${policyName(userId.toString, walletResource.getId)}")
.mapError(UnexpectedError.apply)

permission <- ZIO
.fromOption(permissionOpt)
.orElseFail(PermissionNotFoundById(userId, walletId, walletResource.getId))

_ <- ZIO
.attemptBlocking(
authzClient
.protection()
.policy(walletResource.getId)
.delete(permission.getId)
)
.logError(s"Error while deleting permission ${permission.getId}")
.mapError(UnexpectedError.apply)

_ <- ZIO.log(
s"Permission ${permission.getId} deleted for user ${userId.toString} and wallet ${walletResource.getId}"
)
} yield ()
}
}

object KeycloakPermissionManagementService {
val layer: URLayer[
AuthzClient & WalletManagementService,
PermissionManagement.Service
] =
ZLayer.fromFunction(KeycloakPermissionManagementService(_, _))
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,13 @@ package io.iohk.atala.iam.authorization.keycloak.admin
import io.iohk.atala.sharedtest.containers.{KeycloakContainerCustom, KeycloakTestContainerSupport}
import zio.*
import zio.ZIO.*
import zio.test.*
import zio.test.Assertion.equalTo
import zio.test.TestAspect.*
import zio.test.*

import scala.util.Try

object KeycloakAdminSpec extends ZIOSpecDefault with KeycloakTestContainerSupport {

private def keycloakAdminConfig: RIO[KeycloakContainerCustom, KeycloakAdminConfig] =
for {
keycloakContainer <- ZIO.service[KeycloakContainerCustom]
keycloakAdminConfig = KeycloakAdminConfig(
serverUrl = keycloakContainer.container.getAuthServerUrl,
realm = "master",
username = keycloakContainer.container.getAdminUsername,
password = keycloakContainer.container.getAdminPassword,
clientId = "admin-cli",
clientSecret = Option.empty,
authToken = Option.empty,
scope = Option.empty
)
} yield keycloakAdminConfig

val keycloakAdminConfigLayer = ZLayer.fromZIO(keycloakAdminConfig)
object KeycloakAdminSpec extends ZIOSpecDefault with KeycloakTestContainerSupport with KeycloakConfigUtils {

override def spec = suite("KeycloakAdminSpec")(
test("KeycloakAdmin can be created from the container") {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.iohk.atala.iam.authorization.keycloak.admin

import io.iohk.atala.iam.authentication.oidc.KeycloakConfig
import io.iohk.atala.sharedtest.containers.{KeycloakContainerCustom, KeycloakTestContainerSupport}
import zio.*
import zio.ZIO.*
import zio.test.*

import java.net.URI

trait KeycloakConfigUtils {
this: KeycloakTestContainerSupport =>

protected def keycloakAdminConfig: RIO[KeycloakContainerCustom, KeycloakAdminConfig] =
for {
keycloakContainer <- ZIO.service[KeycloakContainerCustom]
keycloakAdminConfig = KeycloakAdminConfig(
serverUrl = keycloakContainer.container.getAuthServerUrl,
realm = "master",
username = keycloakContainer.container.getAdminUsername,
password = keycloakContainer.container.getAdminPassword,
clientId = "admin-cli",
clientSecret = Option.empty,
authToken = Option.empty,
scope = Option.empty
)
} yield keycloakAdminConfig

protected val keycloakAdminConfigLayer = ZLayer.fromZIO(keycloakAdminConfig)

protected def keycloakConfigLayer(authUpgradeToRPT: Boolean = true) =
ZLayer.fromZIO {
ZIO.serviceWith[KeycloakContainerCustom] { container =>
val host = container.container.getHost()
val port = container.container.getHttpPort()
val url = s"http://${host}:${port}"
KeycloakConfig(
enabled = true,
keycloakUrl = URI(url).toURL(),
realmName = realmName,
clientId = agentClientRepresentation.getClientId(),
clientSecret = agentClientSecret,
autoUpgradeToRPT = authUpgradeToRPT
)
}
}

}
Loading

0 comments on commit 91228ae

Please sign in to comment.