I've created a custom Keycloak provider that listens to a Kafka topic and creates groups that correspond to messages received on the topic.
I've got it mostly working. The only hurdle I still need to overcome is that after I create a group, I don't see it listed in either the admin UI, or in an API call to get the list of groups for the realm. But I do know that these groups are being saved in the database, because if I stop and restart Keycloak, then they're present.
I'm guessing this has to do with the Infinispan cache not being updated. I need to somehow tell the cache to refresh itself. Does anybody know how to do this?
The other thing that's a bit odd is that I had to create my own KeycloakSession for this flow, and a TransactionManager along with it in order to manage the transactions. This is a fairly unique situation, in that the actions being triggered in Keycloak are not associated with a user's session; they're driven off of Kafka messages. Not sure if this has something to do with it.
My setup is like this:
GroupSyncApi.GroupSyncProvider and GroupSyncProviderFactory that extend Provider and ProviderFactory respectivelyGroupSyncProvider has one method: void listen(KeycloakSessionFactory keycloakSessionFactory)KafkaGroupSyncProvider and KafkaGroupSyncProviderFactory to implement my custom interfaces.listen implementation instantiates a class called KafkaTopicListener. This starts a Kafka Consumer and associated code to process the messages in a new thread. The listen method is called from the postInit method of the provider factory. This starts the thread and the Kafka consumer.When a message is received (as JSON), I deserialize it and create a Keycloak group that corresponds to it. I do this as follows (this is in my KafkaTopicListener class:
public void listen(KeycloakSessionFactory keycloakSessionFactory) {
while (true) {
final ConsumerRecords<String, EntityEvent> records = consumer.poll(Duration.of(5, ChronoUnit.SECONDS));
if (!records.isEmpty()) {
KeycloakSession keycloakSession = keycloakSessionFactory.create();
keycloakSession.getTransactionManager().begin();
GroupModel entitiesParent = keycloakSession.realms().getTopLevelGroups(realm).stream().filter(groupModel -> "entities".equals(groupModel.getName())).findFirst().get();
records.forEach(record -> {
logger.info("Received event on Kafka '" + record.topic() + "' topic. " + record.value().getType() + " corresponding entity group with id: " + record.value().getEntity().getId() + "; name: " + record.value().getEntity().getName());
switch (record.value().getType()) {
case CREATE:
doCreate(keycloakSession, realm, record.value(), parent);
break;
}
}
keycloakSession.getTransactionManager().commit();
consumer.commitAsync();
}
}
}
private void doCreate(KeycloakSession keycloakSession, RealmModel realm, EntityEvent event, GroupModel entitiesParent) {
GroupModel group = keycloakSession.realms().createGroup(realm, event.getEntity().getName());
group.setAttribute("type", singleValue("entity"));
group.setParent(entitiesParent);
}
The parts I'm unclear on are:
I found a solution. The issue was with how I assigned my group to its parent group. I did this:
group.setParent(entitiesParent);
But digging through Keycloak source code (I looked at the admin API endpoints in GroupsResource where groups are created), I see that Keycloak instead does:
realm.moveGroup(group, entitiesParent);
The moveGroup method does a bunch of cache management including invalidation of the new group and its parent. This solved my problem; I now see all of my groups in the admin console (following a page reload) after they are created via my provider. The groups are also available via API calls to get the list of groups for the realm.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With