Init
This commit is contained in:
commit
7b8b705f52
13 changed files with 386 additions and 0 deletions
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# sbt specific
|
||||
dist/*
|
||||
target/
|
||||
lib_managed/
|
||||
src_managed/
|
||||
project/boot/
|
||||
project/plugins/project/
|
||||
project/local-plugins.sbt
|
||||
.history
|
||||
.ensime
|
||||
.ensime_cache/
|
||||
.sbt-scripted/
|
||||
local.sbt
|
||||
|
||||
# Bloop
|
||||
.bsp
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
|
||||
# Metals
|
||||
.bloop/
|
||||
.metals/
|
||||
metals.sbt
|
||||
|
||||
# IDEA
|
||||
.idea
|
||||
.idea_modules
|
||||
/.worksheet/
|
2
.scalafmt.conf
Normal file
2
.scalafmt.conf
Normal file
|
@ -0,0 +1,2 @@
|
|||
version = "3.7.15"
|
||||
runner.dialect = scala3
|
25
build.sbt
Normal file
25
build.sbt
Normal file
|
@ -0,0 +1,25 @@
|
|||
val scala3Version = "3.3.1"
|
||||
|
||||
val keycloakVersion = "23.0.6"
|
||||
val keycloakDeps = Seq(
|
||||
"org.keycloak" % "keycloak-core" % keycloakVersion % "provided",
|
||||
"org.keycloak" % "keycloak-server-spi" % 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
|
||||
.in(file("."))
|
||||
.settings(
|
||||
name := "keycloak-event-listener-mqtt",
|
||||
version := keycloakVersion,
|
||||
scalaVersion := scala3Version,
|
||||
resolvers += "Akka library repository".at("https://repo.akka.io/maven"),
|
||||
libraryDependencies ++= keycloakDeps,
|
||||
libraryDependencies ++= deps
|
||||
)
|
1
project/build.properties
Normal file
1
project/build.properties
Normal file
|
@ -0,0 +1 @@
|
|||
sbt.version=1.9.8
|
1
project/plugins.sbt
Normal file
1
project/plugins.sbt
Normal file
|
@ -0,0 +1 @@
|
|||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0")
|
|
@ -0,0 +1 @@
|
|||
net.dergrimm.keycloak.providers.events.mqtt.MqttEventListenerProviderFactory
|
|
@ -0,0 +1,6 @@
|
|||
package net.dergrimm.keycloak.providers.events.mqtt
|
||||
|
||||
private final case class Credentials(
|
||||
username: String,
|
||||
password: String
|
||||
)
|
|
@ -0,0 +1,67 @@
|
|||
package net.dergrimm.keycloak.providers.events.mqtt
|
||||
|
||||
import java.util.logging
|
||||
import org.keycloak.events.EventListenerProvider
|
||||
import org.keycloak.events.Event
|
||||
import org.keycloak.events.EventType
|
||||
import org.keycloak.events.admin.AdminEvent
|
||||
import org.keycloak.events.admin.OperationType
|
||||
import org.keycloak.models.KeycloakSession
|
||||
import akka.stream.scaladsl.Sink
|
||||
import akka.stream.alpakka.mqtt.MqttMessage
|
||||
import akka.Done
|
||||
import scala.concurrent.Future
|
||||
import akka.util.ByteString
|
||||
import akka.stream.scaladsl.Source
|
||||
import scala.util.Success
|
||||
import scala.util.Failure
|
||||
import concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
class MqttEventListenerProvider(
|
||||
val session: KeycloakSession,
|
||||
val excludedEvents: Option[Set[EventType]],
|
||||
val excludedAdminEvents: Option[Set[OperationType]],
|
||||
val mqttOptions: MqttOptions,
|
||||
val mqttSink: Sink[MqttMessage, Future[Done]]
|
||||
) extends EventListenerProvider:
|
||||
private final val logger: logging.Logger =
|
||||
logging.Logger.getLogger(classOf[MqttEventListenerProvider].getName())
|
||||
|
||||
override def onEvent(event: Event): Unit =
|
||||
if excludedEvents.isDefined && excludedEvents.contains(event.getType())
|
||||
then return
|
||||
|
||||
val payload = Payload.fromEvent(event, session)
|
||||
sendMessage(payload)
|
||||
|
||||
override def onEvent(
|
||||
event: AdminEvent,
|
||||
includeRepresentation: Boolean
|
||||
): Unit =
|
||||
if excludedAdminEvents.isDefined && excludedAdminEvents.contains(
|
||||
event.getOperationType()
|
||||
)
|
||||
then return
|
||||
|
||||
val payload = Payload.fromEvent(event, session)
|
||||
sendMessage(payload)
|
||||
|
||||
override def close(): Unit = {}
|
||||
|
||||
private def sendMessage(payload: Payload): Unit =
|
||||
import MqttEventListenerProviderFactory.system
|
||||
|
||||
val topic = s"${mqttOptions.topic}/${payload.topic}"
|
||||
val payloadStr = upickle.default.write(payload)
|
||||
val msg = MqttMessage(topic, ByteString(payloadStr))
|
||||
.withRetained(mqttOptions.retained)
|
||||
val future = Source.single(msg).runWith(mqttSink)
|
||||
future.onComplete {
|
||||
case Success(value) =>
|
||||
logger.log(logging.Level.INFO, value.toString())
|
||||
case Failure(exception) =>
|
||||
logger.log(
|
||||
logging.Level.SEVERE,
|
||||
s"Failed to publish message: ${exception.getMessage()}"
|
||||
)
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package net.dergrimm.keycloak.providers.events.mqtt
|
||||
|
||||
import java.util.logging
|
||||
import org.keycloak.Config
|
||||
import org.keycloak.events.EventListenerProvider
|
||||
import org.keycloak.events.EventListenerProviderFactory
|
||||
import org.keycloak.events.EventType
|
||||
import org.keycloak.events.admin.OperationType
|
||||
import org.keycloak.models.KeycloakSession
|
||||
import org.keycloak.models.KeycloakSessionFactory
|
||||
import scala.collection.immutable
|
||||
import akka.stream.alpakka.mqtt.MqttConnectionSettings
|
||||
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
import java.util.concurrent.TimeUnit
|
||||
import akka.stream.alpakka.mqtt.scaladsl.MqttSink
|
||||
import akka.actor.ActorSystem
|
||||
|
||||
object MqttEventListenerProviderFactory:
|
||||
private val PLUGIN_ID = "mqtt"
|
||||
private val PUBLISHER_ID = "keycloak"
|
||||
|
||||
implicit val system: ActorSystem = ActorSystem()
|
||||
|
||||
class MqttEventListenerProviderFactory(
|
||||
var data: MqttEventListenerProviderFactoryData
|
||||
) extends EventListenerProviderFactory:
|
||||
private final val logger =
|
||||
logging.Logger.getLogger(
|
||||
classOf[MqttEventListenerProviderFactory].getName()
|
||||
)
|
||||
|
||||
def this() = this(null)
|
||||
|
||||
override def create(session: KeycloakSession): MqttEventListenerProvider =
|
||||
MqttEventListenerProvider(
|
||||
session,
|
||||
excludedEvents = data.excludedEvents,
|
||||
excludedAdminEvents = data.excludedAdminOperations,
|
||||
mqttOptions = data.mqttOptions,
|
||||
mqttSink = data.mqttSink
|
||||
)
|
||||
|
||||
override def init(config: Config.Scope): Unit =
|
||||
val excludes = config.getArray("excludeEvents")
|
||||
val excludedEvents =
|
||||
if excludes != null then
|
||||
Some(
|
||||
immutable.HashSet.from(excludes.map(EventType.valueOf))
|
||||
)
|
||||
else None
|
||||
|
||||
val excludesOperations = config.getArray("excludesOperations")
|
||||
val excludedAdminOperations =
|
||||
if excludesOperations != null then
|
||||
Some(
|
||||
immutable.HashSet.from(excludesOperations.map(OperationType.valueOf))
|
||||
)
|
||||
else None
|
||||
|
||||
val uri = config.get("serverUri")
|
||||
if uri == null then
|
||||
throw new IllegalArgumentException("MQTT server URI is null")
|
||||
|
||||
val credentials =
|
||||
val username = config.get("username")
|
||||
val password = config.get("password")
|
||||
if username != null && password != null then
|
||||
Some(Credentials(username, password))
|
||||
else None
|
||||
|
||||
val cleanSession = config.getBoolean("cleanSession", true)
|
||||
val connectionTimeout =
|
||||
FiniteDuration(config.getLong("connectionTimeout", 10), TimeUnit.SECONDS)
|
||||
|
||||
val mqttOptions = MqttOptions.fromConfig(config)
|
||||
var connectionSettings = MqttConnectionSettings(
|
||||
uri,
|
||||
"net.dergrimm.keycloak.providers.events.mqtt",
|
||||
new MemoryPersistence
|
||||
).withCleanSession(cleanSession)
|
||||
.withConnectionTimeout(connectionTimeout)
|
||||
|
||||
credentials match
|
||||
case Some(creds) =>
|
||||
connectionSettings = connectionSettings.withAuth(
|
||||
username = creds.username,
|
||||
password = creds.password
|
||||
)
|
||||
case None => {}
|
||||
|
||||
val sink = MqttSink(connectionSettings, mqttOptions.qos)
|
||||
|
||||
data = MqttEventListenerProviderFactoryData(
|
||||
excludedEvents = excludedEvents,
|
||||
excludedAdminOperations = excludedAdminOperations,
|
||||
mqttOptions,
|
||||
sink
|
||||
)
|
||||
|
||||
override def postInit(factory: KeycloakSessionFactory): Unit = {}
|
||||
|
||||
override def close(): Unit = {}
|
||||
|
||||
override def getId(): String = MqttEventListenerProviderFactory.PLUGIN_ID
|
|
@ -0,0 +1,15 @@
|
|||
package net.dergrimm.keycloak.providers.events.mqtt
|
||||
|
||||
import org.keycloak.events.EventType
|
||||
import org.keycloak.events.admin.OperationType
|
||||
import akka.stream.scaladsl.Sink
|
||||
import akka.stream.alpakka.mqtt.MqttMessage
|
||||
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]]
|
||||
)
|
|
@ -0,0 +1,30 @@
|
|||
package net.dergrimm.keycloak.providers.events.mqtt
|
||||
|
||||
import org.keycloak.Config
|
||||
import akka.stream.alpakka.mqtt.MqttQoS
|
||||
|
||||
object MqttOptions:
|
||||
def fromConfig(config: Config.Scope): MqttOptions =
|
||||
|
||||
val topic = config.get("topic")
|
||||
if topic == null then
|
||||
throw new IllegalArgumentException("MQTT topic is null")
|
||||
|
||||
val retained = config.getBoolean("retained")
|
||||
|
||||
val qos = config.getInt("qos", 0) match
|
||||
case 0 => MqttQoS.atMostOnce
|
||||
case 1 => MqttQoS.atLeastOnce
|
||||
case 2 => MqttQoS.exactlyOnce
|
||||
|
||||
MqttOptions(
|
||||
topic = topic,
|
||||
retained,
|
||||
qos
|
||||
)
|
||||
|
||||
private final case class MqttOptions(
|
||||
topic: String,
|
||||
retained: Boolean,
|
||||
qos: MqttQoS
|
||||
)
|
|
@ -0,0 +1,70 @@
|
|||
package net.dergrimm.keycloak.providers.events.mqtt
|
||||
|
||||
import org.keycloak.events.Event
|
||||
import org.keycloak.events.admin.AdminEvent
|
||||
import org.keycloak.models.KeycloakSession
|
||||
import upickle.default.ReadWriter
|
||||
|
||||
object Payload:
|
||||
def fromEvent(event: Event, session: KeycloakSession): Payload =
|
||||
val error = event.getError()
|
||||
val realmId = event.getRealmId()
|
||||
|
||||
Payload(
|
||||
admin = false,
|
||||
time = event.getTime(),
|
||||
realm = session.realms().getRealm(realmId).getName(),
|
||||
realmId,
|
||||
authDetails = PayloadAuthDetails.fromEvent(event),
|
||||
resourceType = null,
|
||||
operationType = event.getType().toString(),
|
||||
resourcePath = null,
|
||||
representation = null,
|
||||
error,
|
||||
resourceTypeAsString = null
|
||||
)
|
||||
|
||||
def fromEvent(event: AdminEvent, session: KeycloakSession): Payload =
|
||||
val resourceType = event.getResourceType()
|
||||
val representation = event.getRepresentation()
|
||||
val error = event.getError()
|
||||
val realmId = event.getRealmId()
|
||||
|
||||
Payload(
|
||||
admin = true,
|
||||
time = event.getTime(),
|
||||
realm = session.realms().getRealm(realmId).getName(),
|
||||
realmId,
|
||||
authDetails = PayloadAuthDetails.fromEvent(event),
|
||||
resourceType =
|
||||
if resourceType == null then null else resourceType.toString(),
|
||||
operationType = event.getOperationType().toString(),
|
||||
resourcePath = event.getResourcePath(),
|
||||
representation,
|
||||
error,
|
||||
resourceTypeAsString = event.getResourceTypeAsString()
|
||||
)
|
||||
|
||||
private final case class Payload(
|
||||
admin: Boolean,
|
||||
time: Long,
|
||||
realm: String,
|
||||
realmId: String,
|
||||
authDetails: PayloadAuthDetails,
|
||||
resourceType: String,
|
||||
operationType: String,
|
||||
resourcePath: String,
|
||||
representation: String,
|
||||
error: String,
|
||||
resourceTypeAsString: String
|
||||
) derives ReadWriter:
|
||||
private def result: String =
|
||||
if error != null then "error" else "success"
|
||||
|
||||
def topic: String =
|
||||
println(resourceType.toString())
|
||||
if admin
|
||||
then
|
||||
s"admin/${realm}/${result}/${resourceType.toLowerCase()}/${operationType.toLowerCase()}"
|
||||
else
|
||||
s"client/${realm}/${result}/${authDetails.clientId}/${operationType.toLowerCase()}"
|
|
@ -0,0 +1,31 @@
|
|||
package net.dergrimm.keycloak.providers.events.mqtt
|
||||
|
||||
import org.keycloak.events.Event
|
||||
import org.keycloak.events.admin.AdminEvent
|
||||
import upickle.default.ReadWriter
|
||||
|
||||
object PayloadAuthDetails:
|
||||
def fromEvent(event: Event): PayloadAuthDetails =
|
||||
PayloadAuthDetails(
|
||||
realmId = event.getRealmId(),
|
||||
clientId = event.getClientId(),
|
||||
userId = event.getUserId(),
|
||||
ipAddress = event.getIpAddress()
|
||||
)
|
||||
|
||||
def fromEvent(event: AdminEvent): PayloadAuthDetails =
|
||||
val auth = event.getAuthDetails()
|
||||
|
||||
PayloadAuthDetails(
|
||||
realmId = auth.getRealmId(),
|
||||
clientId = auth.getClientId(),
|
||||
userId = auth.getClientId(),
|
||||
ipAddress = auth.getIpAddress()
|
||||
)
|
||||
|
||||
private final case class PayloadAuthDetails(
|
||||
realmId: String,
|
||||
clientId: String,
|
||||
userId: String,
|
||||
ipAddress: String
|
||||
) derives ReadWriter
|
Loading…
Reference in a new issue