Switch JSON serialization framework
This commit is contained in:
parent
2f6537dc8e
commit
2beeb3932f
15
build.sbt
15
build.sbt
|
@ -16,6 +16,13 @@
|
||||||
|
|
||||||
val scala3Version = "3.3.1"
|
val scala3Version = "3.3.1"
|
||||||
|
|
||||||
|
val deps = Seq(
|
||||||
|
"io.bullet" %% "borer-core" % "1.14.0",
|
||||||
|
"io.bullet" %% "borer-derivation" % "1.14.0",
|
||||||
|
"com.lightbend.akka" %% "akka-stream-alpakka-mqtt" % "7.0.1",
|
||||||
|
"com.typesafe.akka" %% "akka-stream" % "2.9.0"
|
||||||
|
)
|
||||||
|
|
||||||
val keycloakVersion = "23.0.6"
|
val keycloakVersion = "23.0.6"
|
||||||
val keycloakDeps = Seq(
|
val keycloakDeps = Seq(
|
||||||
"org.keycloak" % "keycloak-core" % keycloakVersion % "provided",
|
"org.keycloak" % "keycloak-core" % keycloakVersion % "provided",
|
||||||
|
@ -23,17 +30,11 @@ val keycloakDeps = Seq(
|
||||||
"org.keycloak" % "keycloak-server-spi-private" % keycloakVersion % "provided"
|
"org.keycloak" % "keycloak-server-spi-private" % keycloakVersion % "provided"
|
||||||
)
|
)
|
||||||
|
|
||||||
val deps = Seq(
|
|
||||||
"com.lihaoyi" %% "upickle" % "3.2.0",
|
|
||||||
"com.lightbend.akka" %% "akka-stream-alpakka-mqtt" % "7.0.1",
|
|
||||||
"com.typesafe.akka" %% "akka-stream" % "2.9.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
lazy val root = project
|
lazy val root = project
|
||||||
.in(file("."))
|
.in(file("."))
|
||||||
.settings(
|
.settings(
|
||||||
name := "keycloak-event-listener-mqtt",
|
name := "keycloak-event-listener-mqtt",
|
||||||
version := keycloakVersion,
|
version := "0.1.0",
|
||||||
scalaVersion := scala3Version,
|
scalaVersion := scala3Version,
|
||||||
resolvers += "Akka library repository".at("https://repo.akka.io/maven"),
|
resolvers += "Akka library repository".at("https://repo.akka.io/maven"),
|
||||||
libraryDependencies ++= keycloakDeps,
|
libraryDependencies ++= keycloakDeps,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
package net.dergrimm.keycloak.providers.events.mqtt
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2024 Dominic Grimm
|
* Copyright 2024 Dominic Grimm
|
||||||
*
|
*
|
||||||
|
@ -14,18 +16,11 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package net.dergrimm.keycloak.providers.events.mqtt
|
import org.keycloak.events.Event
|
||||||
|
import org.keycloak.events.admin.AdminEvent
|
||||||
|
import org.keycloak.models.KeycloakSession
|
||||||
|
|
||||||
import org.keycloak.events.EventType
|
private trait FromEvent[+Self]:
|
||||||
import org.keycloak.events.admin.OperationType
|
def fromEvent(event: Event, session: KeycloakSession): Self
|
||||||
import akka.stream.scaladsl.Sink
|
def fromEvent(event: AdminEvent, session: KeycloakSession): Self
|
||||||
import akka.stream.alpakka.mqtt.MqttMessage
|
end FromEvent
|
||||||
import akka.Done
|
|
||||||
import scala.concurrent.Future
|
|
||||||
|
|
||||||
final case class MqttEventListenerProviderFactoryData(
|
|
||||||
excludedEvents: Option[Set[EventType]],
|
|
||||||
excludedAdminOperations: Option[Set[OperationType]],
|
|
||||||
mqttOptions: MqttOptions,
|
|
||||||
mqttSink: Sink[MqttMessage, Future[Done]]
|
|
||||||
)
|
|
|
@ -31,6 +31,7 @@ import akka.util.ByteString
|
||||||
import akka.stream.scaladsl.Source
|
import akka.stream.scaladsl.Source
|
||||||
import scala.util.Success
|
import scala.util.Success
|
||||||
import scala.util.Failure
|
import scala.util.Failure
|
||||||
|
import io.bullet.borer.Json
|
||||||
|
|
||||||
class MqttEventListenerProvider(
|
class MqttEventListenerProvider(
|
||||||
val session: KeycloakSession,
|
val session: KeycloakSession,
|
||||||
|
@ -46,8 +47,9 @@ class MqttEventListenerProvider(
|
||||||
if excludedEvents.isDefined && excludedEvents.contains(event.getType())
|
if excludedEvents.isDefined && excludedEvents.contains(event.getType())
|
||||||
then return
|
then return
|
||||||
|
|
||||||
val payload = Payload.fromEvent(event, session)
|
val payload: Payload = Payload.fromEvent(event, session)
|
||||||
sendMessage(payload)
|
sendMessage(payload)
|
||||||
|
end onEvent
|
||||||
|
|
||||||
override def onEvent(
|
override def onEvent(
|
||||||
event: AdminEvent,
|
event: AdminEvent,
|
||||||
|
@ -58,8 +60,9 @@ class MqttEventListenerProvider(
|
||||||
)
|
)
|
||||||
then return
|
then return
|
||||||
|
|
||||||
val payload = Payload.fromEvent(event, session)
|
val payload: Payload = Payload.fromEvent(event, session)
|
||||||
sendMessage(payload)
|
sendMessage(payload)
|
||||||
|
end onEvent
|
||||||
|
|
||||||
override def close(): Unit = {}
|
override def close(): Unit = {}
|
||||||
|
|
||||||
|
@ -67,11 +70,14 @@ class MqttEventListenerProvider(
|
||||||
import concurrent.ExecutionContext.Implicits.global
|
import concurrent.ExecutionContext.Implicits.global
|
||||||
import MqttEventListenerProviderFactory.system
|
import MqttEventListenerProviderFactory.system
|
||||||
|
|
||||||
val topic = s"${mqttOptions.topic}/${payload.topic}"
|
val topic: String = s"${mqttOptions.topic}/${payload.topic}"
|
||||||
val payloadStr = upickle.default.write(payload)
|
val payloadBytes: Array[Byte] = Json.encode(payload).toByteArray
|
||||||
val msg = MqttMessage(topic, ByteString(payloadStr))
|
|
||||||
.withRetained(mqttOptions.retained)
|
val msg: MqttMessage =
|
||||||
val future = Source.single(msg).runWith(mqttSink)
|
MqttMessage(topic, ByteString.fromArray(payloadBytes))
|
||||||
|
.withRetained(mqttOptions.retained)
|
||||||
|
val future: Future[Done] = Source.single(msg).runWith(mqttSink)
|
||||||
|
|
||||||
future.onComplete {
|
future.onComplete {
|
||||||
case Failure(exception) =>
|
case Failure(exception) =>
|
||||||
logger.log(
|
logger.log(
|
||||||
|
@ -80,4 +86,5 @@ class MqttEventListenerProvider(
|
||||||
)
|
)
|
||||||
case Success(_) => {}
|
case Success(_) => {}
|
||||||
}
|
}
|
||||||
|
end sendMessage
|
||||||
end MqttEventListenerProvider
|
end MqttEventListenerProvider
|
||||||
|
|
|
@ -31,70 +31,82 @@ import scala.concurrent.duration.FiniteDuration
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import akka.stream.alpakka.mqtt.scaladsl.MqttSink
|
import akka.stream.alpakka.mqtt.scaladsl.MqttSink
|
||||||
import akka.actor.ActorSystem
|
import akka.actor.ActorSystem
|
||||||
|
import akka.stream.scaladsl.Sink
|
||||||
|
import akka.stream.alpakka.mqtt.MqttMessage
|
||||||
|
import akka.Done
|
||||||
|
import scala.concurrent.Future
|
||||||
|
|
||||||
object MqttEventListenerProviderFactory:
|
object MqttEventListenerProviderFactory:
|
||||||
private val PLUGIN_ID = "mqtt"
|
private val PLUGIN_ID = "mqtt"
|
||||||
private val PUBLISHER_ID = "keycloak"
|
private val PUBLISHER_ID = "keycloak"
|
||||||
|
|
||||||
implicit val system: ActorSystem = ActorSystem()
|
given system: ActorSystem = ActorSystem()
|
||||||
end MqttEventListenerProviderFactory
|
end MqttEventListenerProviderFactory
|
||||||
|
|
||||||
class MqttEventListenerProviderFactory(
|
class MqttEventListenerProviderFactory extends EventListenerProviderFactory:
|
||||||
var data: MqttEventListenerProviderFactoryData
|
|
||||||
) extends EventListenerProviderFactory:
|
|
||||||
private final val logger =
|
private final val logger =
|
||||||
logging.Logger.getLogger(
|
logging.Logger.getLogger(
|
||||||
classOf[MqttEventListenerProviderFactory].getName()
|
classOf[MqttEventListenerProviderFactory].getName()
|
||||||
)
|
)
|
||||||
|
|
||||||
def this() = this(null)
|
private var excludedEvents: Option[Set[EventType]] = None
|
||||||
|
private var excludedAdminEvents: Option[Set[OperationType]] = None
|
||||||
|
private var mqttOptions: MqttOptions = null
|
||||||
|
private var mqttSink: Sink[MqttMessage, Future[Done]] = null
|
||||||
|
|
||||||
override def create(session: KeycloakSession): MqttEventListenerProvider =
|
override def create(session: KeycloakSession): MqttEventListenerProvider =
|
||||||
MqttEventListenerProvider(
|
MqttEventListenerProvider(
|
||||||
session,
|
session,
|
||||||
excludedEvents = data.excludedEvents,
|
excludedEvents,
|
||||||
excludedAdminEvents = data.excludedAdminOperations,
|
excludedAdminEvents,
|
||||||
mqttOptions = data.mqttOptions,
|
mqttOptions,
|
||||||
mqttSink = data.mqttSink
|
mqttSink
|
||||||
)
|
)
|
||||||
|
end create
|
||||||
|
|
||||||
override def init(config: Config.Scope): Unit =
|
override def init(config: Config.Scope): Unit =
|
||||||
val excludes = config.getArray("excludeEvents")
|
val excludes: Array[String] = config.getArray("excludeEvents")
|
||||||
val excludedEvents =
|
excludedEvents =
|
||||||
if excludes != null then
|
if excludes != null
|
||||||
|
then
|
||||||
Some(
|
Some(
|
||||||
immutable.HashSet.from(excludes.map(EventType.valueOf))
|
immutable.HashSet.from(excludes.map(EventType.valueOf))
|
||||||
)
|
)
|
||||||
else None
|
else None
|
||||||
|
|
||||||
val excludesOperations = config.getArray("excludesOperations")
|
val excludesOperations: Array[String] =
|
||||||
val excludedAdminOperations =
|
config.getArray("excludesOperations")
|
||||||
if excludesOperations != null then
|
excludedAdminEvents =
|
||||||
|
if excludesOperations != null
|
||||||
|
then
|
||||||
Some(
|
Some(
|
||||||
immutable.HashSet.from(excludesOperations.map(OperationType.valueOf))
|
immutable.HashSet.from(excludesOperations.map(OperationType.valueOf))
|
||||||
)
|
)
|
||||||
else None
|
else None
|
||||||
|
|
||||||
val uri = config.get("serverUri")
|
val uri: String = config.get("serverUri")
|
||||||
if uri == null then
|
if uri == null then
|
||||||
throw new IllegalArgumentException("MQTT server URI is null")
|
throw new IllegalArgumentException("MQTT server URI is null")
|
||||||
|
|
||||||
val credentials =
|
val credentials: Option[(String, String)] =
|
||||||
val username = config.get("username")
|
val username = config.get("username")
|
||||||
val password = config.get("password")
|
val password = config.get("password")
|
||||||
if username != null && password != null then Some((username, password))
|
|
||||||
|
if username != null && password != null
|
||||||
|
then Some(username -> password)
|
||||||
else None
|
else None
|
||||||
|
|
||||||
val cleanSession = config.getBoolean("cleanSession", true)
|
val cleanSession: Boolean = config.getBoolean("cleanSession", true)
|
||||||
val connectionTimeout =
|
val connectionTimeout: FiniteDuration =
|
||||||
FiniteDuration(config.getLong("connectionTimeout", 10), TimeUnit.SECONDS)
|
FiniteDuration(config.getLong("connectionTimeout", 10), TimeUnit.SECONDS)
|
||||||
|
|
||||||
val mqttOptions = MqttOptions.fromConfig(config)
|
mqttOptions = MqttOptions.fromConfig(config)
|
||||||
var connectionSettings = MqttConnectionSettings(
|
var connectionSettings = MqttConnectionSettings(
|
||||||
uri,
|
uri,
|
||||||
"net.dergrimm.keycloak.providers.events.mqtt",
|
"net.dergrimm.keycloak.providers.events.mqtt",
|
||||||
new MemoryPersistence
|
new MemoryPersistence()
|
||||||
).withCleanSession(cleanSession)
|
)
|
||||||
|
.withCleanSession(cleanSession)
|
||||||
.withConnectionTimeout(connectionTimeout)
|
.withConnectionTimeout(connectionTimeout)
|
||||||
|
|
||||||
credentials match
|
credentials match
|
||||||
|
@ -105,14 +117,8 @@ class MqttEventListenerProviderFactory(
|
||||||
)
|
)
|
||||||
case None => {}
|
case None => {}
|
||||||
|
|
||||||
val sink = MqttSink(connectionSettings, mqttOptions.qos)
|
mqttSink = MqttSink(connectionSettings, mqttOptions.qos)
|
||||||
|
end init
|
||||||
data = MqttEventListenerProviderFactoryData(
|
|
||||||
excludedEvents = excludedEvents,
|
|
||||||
excludedAdminOperations = excludedAdminOperations,
|
|
||||||
mqttOptions,
|
|
||||||
sink
|
|
||||||
)
|
|
||||||
|
|
||||||
override def postInit(factory: KeycloakSessionFactory): Unit = {}
|
override def postInit(factory: KeycloakSessionFactory): Unit = {}
|
||||||
|
|
||||||
|
|
|
@ -21,23 +21,23 @@ import akka.stream.alpakka.mqtt.MqttQoS
|
||||||
|
|
||||||
object MqttOptions:
|
object MqttOptions:
|
||||||
def fromConfig(config: Config.Scope): MqttOptions =
|
def fromConfig(config: Config.Scope): MqttOptions =
|
||||||
|
val topic: String = config.get("topic")
|
||||||
val topic = config.get("topic")
|
|
||||||
if topic == null then
|
if topic == null then
|
||||||
throw new IllegalArgumentException("MQTT topic is null")
|
throw new IllegalArgumentException("MQTT topic is null")
|
||||||
|
|
||||||
val retained = config.getBoolean("retained")
|
val retained: Boolean = config.getBoolean("retained")
|
||||||
|
|
||||||
val qos = config.getInt("qos", 0) match
|
val qos: MqttQoS = config.getInt("qos", 0) match
|
||||||
case 0 => MqttQoS.atMostOnce
|
case 0 => MqttQoS.atMostOnce
|
||||||
case 1 => MqttQoS.atLeastOnce
|
case 1 => MqttQoS.atLeastOnce
|
||||||
case 2 => MqttQoS.exactlyOnce
|
case 2 => MqttQoS.exactlyOnce
|
||||||
|
|
||||||
MqttOptions(
|
MqttOptions(
|
||||||
topic = topic,
|
topic,
|
||||||
retained,
|
retained,
|
||||||
qos
|
qos
|
||||||
)
|
)
|
||||||
|
end fromConfig
|
||||||
end MqttOptions
|
end MqttOptions
|
||||||
|
|
||||||
private final case class MqttOptions(
|
private final case class MqttOptions(
|
||||||
|
|
|
@ -19,47 +19,50 @@ package net.dergrimm.keycloak.providers.events.mqtt
|
||||||
import org.keycloak.events.Event
|
import org.keycloak.events.Event
|
||||||
import org.keycloak.events.admin.AdminEvent
|
import org.keycloak.events.admin.AdminEvent
|
||||||
import org.keycloak.models.KeycloakSession
|
import org.keycloak.models.KeycloakSession
|
||||||
import upickle.default.ReadWriter
|
import org.keycloak.events.admin.ResourceType
|
||||||
|
import io.bullet.borer.{Codec, Encoder}
|
||||||
|
import io.bullet.borer.derivation.MapBasedCodecs._
|
||||||
|
import io.bullet.borer.NullOptions.given
|
||||||
|
|
||||||
object Payload:
|
object Payload extends FromEvent[Payload]:
|
||||||
def fromEvent(event: Event, session: KeycloakSession): Payload =
|
def fromEvent(event: Event, session: KeycloakSession): Payload =
|
||||||
val error = event.getError()
|
val error: Option[String] = Option(event.getError())
|
||||||
val realmId = event.getRealmId()
|
val realmId: String = event.getRealmId()
|
||||||
|
|
||||||
Payload(
|
Payload(
|
||||||
admin = false,
|
admin = false,
|
||||||
time = event.getTime(),
|
time = event.getTime(),
|
||||||
realm = session.realms().getRealm(realmId).getName(),
|
realm = session.realms().getRealm(realmId).getName(),
|
||||||
realmId,
|
realmId,
|
||||||
authDetails = PayloadAuthDetails.fromEvent(event),
|
authDetails = PayloadAuthDetails.fromEvent(event, session),
|
||||||
resourceType = null,
|
resourceType = None,
|
||||||
operationType = event.getType().toString(),
|
operationType = event.getType().toString(),
|
||||||
resourcePath = null,
|
resourcePath = None,
|
||||||
representation = null,
|
representation = None,
|
||||||
error,
|
error
|
||||||
resourceTypeAsString = null
|
|
||||||
)
|
)
|
||||||
|
end fromEvent
|
||||||
|
|
||||||
def fromEvent(event: AdminEvent, session: KeycloakSession): Payload =
|
def fromEvent(event: AdminEvent, session: KeycloakSession): Payload =
|
||||||
val resourceType = event.getResourceType()
|
val resourceType: Option[String] =
|
||||||
val representation = event.getRepresentation()
|
Option(event.getResourceType()).map(_.toString())
|
||||||
val error = event.getError()
|
val representation: Option[String] = Option(event.getRepresentation())
|
||||||
val realmId = event.getRealmId()
|
val error: Option[String] = Option(event.getError())
|
||||||
|
val realmId: String = event.getRealmId()
|
||||||
|
|
||||||
Payload(
|
Payload(
|
||||||
admin = true,
|
admin = true,
|
||||||
time = event.getTime(),
|
time = event.getTime(),
|
||||||
realm = session.realms().getRealm(realmId).getName(),
|
realm = session.realms().getRealm(realmId).getName(),
|
||||||
realmId,
|
realmId,
|
||||||
authDetails = PayloadAuthDetails.fromEvent(event),
|
authDetails = PayloadAuthDetails.fromEvent(event, session),
|
||||||
resourceType =
|
resourceType,
|
||||||
if resourceType == null then null else resourceType.toString(),
|
|
||||||
operationType = event.getOperationType().toString(),
|
operationType = event.getOperationType().toString(),
|
||||||
resourcePath = event.getResourcePath(),
|
resourcePath = Some(event.getResourcePath()),
|
||||||
representation,
|
representation,
|
||||||
error,
|
error
|
||||||
resourceTypeAsString = event.getResourceTypeAsString()
|
|
||||||
)
|
)
|
||||||
|
end fromEvent
|
||||||
end Payload
|
end Payload
|
||||||
|
|
||||||
private final case class Payload(
|
private final case class Payload(
|
||||||
|
@ -68,20 +71,22 @@ private final case class Payload(
|
||||||
realm: String,
|
realm: String,
|
||||||
realmId: String,
|
realmId: String,
|
||||||
authDetails: PayloadAuthDetails,
|
authDetails: PayloadAuthDetails,
|
||||||
resourceType: String,
|
resourceType: Option[String],
|
||||||
operationType: String,
|
operationType: String,
|
||||||
resourcePath: String,
|
resourcePath: Option[String],
|
||||||
representation: String,
|
representation: Option[String],
|
||||||
error: String,
|
error: Option[String]
|
||||||
resourceTypeAsString: String
|
) derives Codec:
|
||||||
) derives ReadWriter:
|
private def result: String = if error.isDefined then "error" else "success"
|
||||||
private def result: String =
|
|
||||||
if error != null then "error" else "success"
|
|
||||||
|
|
||||||
def topic: String =
|
def topic: String =
|
||||||
println(resourceType.toString())
|
|
||||||
if admin
|
if admin
|
||||||
then
|
then
|
||||||
s"admin/${realm}/${result}/${resourceType.toLowerCase()}/${operationType.toLowerCase()}"
|
resourceType match
|
||||||
|
case Some(rType) =>
|
||||||
|
s"admin/${realm}/${result}/${rType.toLowerCase()}/${operationType.toLowerCase()}"
|
||||||
|
case None => throw new IllegalStateException
|
||||||
else
|
else
|
||||||
s"client/${realm}/${result}/${authDetails.clientId}/${operationType.toLowerCase()}"
|
s"client/${realm}/${result}/${authDetails.clientId}/${operationType.toLowerCase()}"
|
||||||
|
end topic
|
||||||
|
end Payload
|
||||||
|
|
|
@ -17,20 +17,27 @@
|
||||||
package net.dergrimm.keycloak.providers.events.mqtt
|
package net.dergrimm.keycloak.providers.events.mqtt
|
||||||
|
|
||||||
import org.keycloak.events.Event
|
import org.keycloak.events.Event
|
||||||
import org.keycloak.events.admin.AdminEvent
|
import org.keycloak.events.admin.{AdminEvent, AuthDetails}
|
||||||
import upickle.default.ReadWriter
|
import org.keycloak.models.KeycloakSession
|
||||||
|
import io.bullet.borer.Codec
|
||||||
|
import io.bullet.borer.derivation.MapBasedCodecs._
|
||||||
|
import io.bullet.borer.NullOptions.given
|
||||||
|
|
||||||
object PayloadAuthDetails:
|
object PayloadAuthDetails extends FromEvent[PayloadAuthDetails]:
|
||||||
def fromEvent(event: Event): PayloadAuthDetails =
|
def fromEvent(event: Event, session: KeycloakSession): PayloadAuthDetails =
|
||||||
PayloadAuthDetails(
|
PayloadAuthDetails(
|
||||||
realmId = event.getRealmId(),
|
realmId = event.getRealmId(),
|
||||||
clientId = event.getClientId(),
|
clientId = event.getClientId(),
|
||||||
userId = event.getUserId(),
|
userId = event.getUserId(),
|
||||||
ipAddress = event.getIpAddress()
|
ipAddress = event.getIpAddress()
|
||||||
)
|
)
|
||||||
|
end fromEvent
|
||||||
|
|
||||||
def fromEvent(event: AdminEvent): PayloadAuthDetails =
|
def fromEvent(
|
||||||
val auth = event.getAuthDetails()
|
event: AdminEvent,
|
||||||
|
session: KeycloakSession
|
||||||
|
): PayloadAuthDetails =
|
||||||
|
val auth: AuthDetails = event.getAuthDetails()
|
||||||
|
|
||||||
PayloadAuthDetails(
|
PayloadAuthDetails(
|
||||||
realmId = auth.getRealmId(),
|
realmId = auth.getRealmId(),
|
||||||
|
@ -38,6 +45,7 @@ object PayloadAuthDetails:
|
||||||
userId = auth.getClientId(),
|
userId = auth.getClientId(),
|
||||||
ipAddress = auth.getIpAddress()
|
ipAddress = auth.getIpAddress()
|
||||||
)
|
)
|
||||||
|
end fromEvent
|
||||||
end PayloadAuthDetails
|
end PayloadAuthDetails
|
||||||
|
|
||||||
private final case class PayloadAuthDetails(
|
private final case class PayloadAuthDetails(
|
||||||
|
@ -45,4 +53,4 @@ private final case class PayloadAuthDetails(
|
||||||
clientId: String,
|
clientId: String,
|
||||||
userId: String,
|
userId: String,
|
||||||
ipAddress: String
|
ipAddress: String
|
||||||
) derives ReadWriter
|
) derives Codec
|
||||||
|
|
Loading…
Reference in a new issue