Skip to content

Commit

Permalink
feat: Keycloak container support with clients and PermissionManagemen…
Browse files Browse the repository at this point in the history
…t service (#755)

Signed-off-by: Yurii Shynbuiev <[email protected]>
Signed-off-by: Pat Losoponkul <[email protected]>
Co-authored-by: Pat Losoponkul <[email protected]>
  • Loading branch information
yshyn-iohk and Pat Losoponkul authored Nov 6, 2023
1 parent a792f43 commit a1846aa
Show file tree
Hide file tree
Showing 47 changed files with 1,180 additions and 53 deletions.
40 changes: 40 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ lazy val V = new {
val typesafeConfig = "1.4.2"
val protobuf = "3.1.9"
val testContainersScala = "0.41.0"
val testContainersJavaKeycloak = "3.0.0"

val doobie = "1.0.0-RC2"
val quill = "4.7.3"
Expand Down Expand Up @@ -127,6 +128,7 @@ lazy val D = new {
// TODO we are adding test stuff to the main dependencies
val testcontainersPostgres: ModuleID = "com.dimafeng" %% "testcontainers-scala-postgresql" % V.testContainersScala
val testcontainersVault: ModuleID = "com.dimafeng" %% "testcontainers-scala-vault" % V.testContainersScala
val testcontainersKeycloak: ModuleID = "com.github.dasniko" % "testcontainers-keycloak" % V.testContainersJavaKeycloak

val doobiePostgres: ModuleID = "org.tpolecat" %% "doobie-postgres" % V.doobie
val doobieHikari: ModuleID = "org.tpolecat" %% "doobie-hikari" % V.doobie
Expand Down Expand Up @@ -155,6 +157,7 @@ lazy val D_Shared = new {
D.scalaPbGrpc,
D.testcontainersPostgres,
D.testcontainersVault,
D.testcontainersKeycloak,
D.zio,
// FIXME: split shared DB stuff as subproject?
D.doobieHikari,
Expand All @@ -163,6 +166,26 @@ lazy val D_Shared = new {
)
}

lazy val D_SharedTest = new {
lazy val dependencies: Seq[ModuleID] =
Seq(
D.typesafeConfig,
D.testcontainersPostgres,
D.testcontainersVault,
D.testcontainersKeycloak,
D.zio,
D.doobieHikari,
D.doobiePostgres,
D.zioCatsInterop,
D.zioJson,
D.zioHttp,
D.zioTest,
D.zioTestSbt,
D.zioTestMagnolia,
D.zioMock
)
}

lazy val D_Connect = new {

private lazy val logback = "ch.qos.logback" % "logback-classic" % V.logback % Test
Expand Down Expand Up @@ -403,6 +426,18 @@ lazy val shared = (project in file("shared"))
)
.enablePlugins(BuildInfoPlugin)

lazy val sharedTest = (project in file("shared-test"))
.settings(
organization := "io.iohk.atala",
organizationName := "Input Output Global",
buildInfoPackage := "io.iohk.atala.sharedtest",
name := "sharedtest",
crossPaths := false,
libraryDependencies ++= D_SharedTest.dependencies
)
.dependsOn(shared)
.enablePlugins(BuildInfoPlugin)

// #########################
// ### Models & Services ###
// #########################
Expand Down Expand Up @@ -676,6 +711,7 @@ lazy val polluxDoobie = project
)
.dependsOn(polluxCore % "compile->compile;test->test")
.dependsOn(shared)
.dependsOn(sharedTest % Test)

// ########################
// ### Pollux Anoncreds ###
Expand Down Expand Up @@ -725,6 +761,7 @@ lazy val connectDoobie = project
libraryDependencies ++= D_Connect.sqlDoobieDependencies
)
.dependsOn(shared)
.dependsOn(sharedTest % Test)
.dependsOn(connectCore % "compile->compile;test->test")

// ############################
Expand Down Expand Up @@ -760,6 +797,7 @@ lazy val prismAgentWalletAPI = project
castorCore,
eventNotification
)
.dependsOn(sharedTest % Test)

lazy val prismAgentServer = project
.in(file("prism-agent/service/server"))
Expand Down Expand Up @@ -792,6 +830,7 @@ lazy val prismAgentServer = project
castorCore,
eventNotification
)
.dependsOn(sharedTest % Test)

// ############################
// #### Release process #####
Expand All @@ -809,6 +848,7 @@ releaseProcess := Seq[ReleaseStep](

lazy val aggregatedProjects: Seq[ProjectReference] = Seq(
shared,
sharedTest,
models,
protocolConnection,
protocolCoordinateMediation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.iohk.atala.connect.sql.repository
import com.dimafeng.testcontainers.PostgreSQLContainer
import io.iohk.atala.connect.core.repository.{ConnectionRepository, ConnectionRepositorySpecSuite}
import io.iohk.atala.shared.db.DbConfig
import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport
import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport
import zio.*
import zio.test.*

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package io.iohk.atala.test.container

import com.dimafeng.testcontainers.PostgreSQLContainer
import io.iohk.atala.sharedtest.containers.PostgresTestContainer.postgresContainer
import zio.*
import zio.ZIO.*
import io.iohk.atala.shared.test.containers.PostgresTestContainer.postgresContainer

object PostgresLayer {

Expand Down
1 change: 0 additions & 1 deletion 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
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import io.iohk.atala.pollux.sql.model.db.{CredentialDefinition, CredentialDefini
import io.iohk.atala.shared.db.ContextAwareTask
import io.iohk.atala.shared.db.Implicits.*
import io.iohk.atala.shared.models.{WalletAccessContext, WalletId}
import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport
import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport
import io.iohk.atala.test.container.MigrationAspects.*
import zio.*
import zio.json.ast.Json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import io.iohk.atala.shared.db.ContextAwareTask
import io.iohk.atala.shared.db.Implicits.*
import io.iohk.atala.shared.models.WalletAccessContext
import io.iohk.atala.shared.models.WalletId
import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport
import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport
import io.iohk.atala.test.container.MigrationAspects.*
import zio.*
import zio.json.ast.Json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import io.iohk.atala.shared.db.ContextAwareTask
import io.iohk.atala.shared.db.Implicits.*
import io.iohk.atala.shared.models.WalletAccessContext
import io.iohk.atala.shared.models.WalletId
import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport
import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport
import io.iohk.atala.test.container.MigrationAspects.*
import zio.*
import zio.test.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.iohk.atala.pollux.sql.repository
import com.dimafeng.testcontainers.PostgreSQLContainer
import io.iohk.atala.pollux.core.repository._
import io.iohk.atala.shared.db.DbConfig
import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport
import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport
import zio._
import zio.test._

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.iohk.atala.pollux.sql.repository
import com.dimafeng.testcontainers.PostgreSQLContainer
import io.iohk.atala.pollux.core.repository._
import io.iohk.atala.shared.db.DbConfig
import io.iohk.atala.shared.test.containers.PostgresTestContainerSupport
import io.iohk.atala.sharedtest.containers.PostgresTestContainerSupport
import zio._
import zio.test._

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,6 @@ object JWTVerificationTest extends ZIOSpecDefault {
validation <- JwtCredential.validateEncodedJWT(jwtCredential)(resolver)
} yield assert(validation.fold(_ => false, _ => true))(equalTo(false))
}
).when(!sys.props.get("os.name").contains("Mac OS X")) // Mac OS X throws `Curve not supported: secp256k1`
)

}
19 changes: 19 additions & 0 deletions prism-agent/service/server/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,22 @@ agent {
authApiKey = ${?DEFAULT_WALLET_AUTH_API_KEY}
}
}

keycloakAdmin {
serverUrl = "http://localhost:8080/auth",
serverUrl = ${?KEYCLOAK_SERVER_URL}
realm = "master",
realm = ${?KEYCLOAK_REALM}
username = "admin",
username = ${?KEYCLOAK_ADMIN_USERNAME}
password = "admin",
password = ${?KEYCLOAK_ADMIN_PASSWORD}
clientId = "admin-cli",
clientId = ${?KEYCLOAK_ADMIN_CLIENT_ID}
clientSecret = "",
clientSecret = ${?KEYCLOAK_ADMIN_CLIENT_SECRET}
authToken= "",
authToken = ${?KEYCLOAK_ADMIN_AUTH_TOKEN}
scope= ""
scope = ${?KEYCLOAK_ADMIN_SCOPE}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.iohk.atala.agent.server.config

import io.iohk.atala.castor.core.model.did.VerificationRelationship
import io.iohk.atala.iam.authentication.AuthenticationConfig
import io.iohk.atala.iam.authorization.keycloak.admin.KeycloakAdminConfig
import io.iohk.atala.pollux.vc.jwt.*
import io.iohk.atala.shared.db.DbConfig
import zio.config.*
Expand All @@ -17,6 +18,7 @@ final case class AppConfig(
agent: AgentConfig,
connect: ConnectConfig,
prismNode: PrismNodeConfig,
keycloakAdmin: KeycloakAdminConfig
) {
def validate: Either[String, Unit] =
for {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,19 @@ object TokenIntrospection {
given JsonDecoder[TokenIntrospection] = JsonDecoder.derived
}

final case class TokenResponse(access_token: String, refresh_token: String)

object TokenResponse {
given JsonEncoder[TokenResponse] = JsonEncoder.derived
given JsonDecoder[TokenResponse] = JsonDecoder.derived
}

trait KeycloakClient {

def getRpt(accessToken: String): IO[AuthenticationError, String]

def getAccessToken(username: String, password: String): IO[AuthenticationError, TokenResponse]

def introspectToken(token: String): IO[AuthenticationError, TokenIntrospection]

/** Return list of permitted resources */
Expand All @@ -32,6 +41,7 @@ class KeycloakClientImpl(client: AuthzClient, httpClient: Client, keycloakConfig
extends KeycloakClient {

private val introspectionUrl = client.getServerConfiguration().getIntrospectionEndpoint()
private val tokenUrl = client.getServerConfiguration().getTokenEndpoint()

private val baseFormHeaders = Headers(Header.ContentType(MediaType.application.`x-www-form-urlencoded`))

Expand Down Expand Up @@ -72,6 +82,42 @@ class KeycloakClientImpl(client: AuthzClient, httpClient: Client, keycloakConfig
} yield result
}

override def getAccessToken(username: String, password: String): IO[AuthenticationError, TokenResponse] = {
for {
response <- Client
.request(
url = tokenUrl,
method = Method.POST,
headers = baseFormHeaders,
content = Body.fromURLEncodedForm(
Form(
FormField.simpleField("grant_type", "password"),
FormField.simpleField("client_id", keycloakConfig.clientId),
FormField.simpleField("client_secret", keycloakConfig.clientSecret),
FormField.simpleField("username", username),
FormField.simpleField("password", password),
)
)
)
.logError("Fail to get the accessToken on keyclaok.")
.mapError(e => AuthenticationError.UnexpectedError("Fail to get the accessToken on keyclaok."))
.provide(ZLayer.succeed(httpClient))
body <- response.body.asString
.logError("Fail parse keycloak token response.")
.mapError(e => AuthenticationError.UnexpectedError("Fail parse keycloak token response."))
result <-
if (response.status.code == 200) {
ZIO
.fromEither(body.fromJson[TokenResponse])
.logError("Fail to decode keycloak token response")
.mapError(e => AuthenticationError.UnexpectedError(e))
} else {
ZIO.logError(s"Keycloak token introspection was unsucessful. Status: ${response.status}. Response: $body") *>
ZIO.fail(AuthenticationError.UnexpectedError("Token introspection was unsuccessful."))
}
} yield result
}

override def getRpt(accessToken: String): IO[AuthenticationError, String] =
ZIO
.attemptBlocking {
Expand All @@ -97,9 +143,11 @@ class KeycloakClientImpl(client: AuthzClient, httpClient: Client, keycloakConfig
}

object KeycloakClientImpl {
val layer: RLayer[KeycloakConfig & Client, KeycloakClient] = ZLayer.fromZIO {
val layer: RLayer[KeycloakConfig & Client, KeycloakClient] =
authzClientLayer >>> ZLayer.fromFunction(KeycloakClientImpl(_, _, _))

def authzClientLayer: RLayer[KeycloakConfig, AuthzClient] = ZLayer.fromZIO {
for {
httpClient <- ZIO.service[Client]
keycloakConfig <- ZIO.service[KeycloakConfig]
config = KeycloakAuthzConfig(
keycloakConfig.keycloakUrl.toString(),
Expand All @@ -109,7 +157,6 @@ object KeycloakClientImpl {
null
)
client <- ZIO.attempt(AuthzClient.create(config))
} yield KeycloakClientImpl(client, httpClient, keycloakConfig)
} yield client
}

}
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)
}
}
Loading

0 comments on commit a1846aa

Please sign in to comment.