-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(prism-agent): implement the logic to grant and revoke permission…
…s for the user to have access to the wallet Signed-off-by: Yurii Shynbuiev <yurii.shynbuiev@iohk.io>
- Loading branch information
1 parent
6d8d05b
commit 91228ae
Showing
10 changed files
with
462 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
...ice/server/src/main/scala/io/iohk/atala/iam/authorization/core/PermissionManagement.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
159 changes: 153 additions & 6 deletions
159
.../io/iohk/atala/iam/authorization/keycloak/admin/KeycloakPermissionManagementService.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(_, _)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
...r/src/test/scala/io/iohk/atala/iam/authorization/keycloak/admin/KeycloakConfigUtils.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.