Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/guides/admin-api/index.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
= Keycloak Admin API Guide

include::../attributes.adoc[]

<#list ctx.guides as guide>
:links_admin-api_${guide.id}_name: ${guide.title}
:links_admin-api_${guide.id}_url: #${guide.id}
</#list>

<#list ctx.guides as guide>
include::${guide.template}[leveloffset=+${guide.levelOffset}]
</#list>
1 change: 1 addition & 0 deletions docs/guides/admin-api/pinned-guides
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
protocol-mappers
61 changes: 61 additions & 0 deletions docs/guides/admin-api/protocol-mappers.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<#import "/templates/guide.adoc" as tmpl>

<@tmpl.guide
title="Protocol Mappers"
summary="Provided ProtocolMapper implementations.">

A {project_name} `ProtocolMapper` is a service provider interface (SPI) component used to map user data (attributes, roles, session notes)
into tokens for OpenID Connect or SAML protocols. They enable customizing token contents to include specific information
required by client applications.

Protocol mappers can be created and managed via the {project_name} REST API using the
link:https://www.keycloak.org/docs-api/latest/rest-api/index.html#_post_adminrealmsrealmclient_scopesclient_scope_idprotocol_mappersmodels[Create protocol mapper] endpoint.
When creating a `ProtocolMapperRepresentation`, the `config` field is a key-value map whose available entries
depend on the specific mapper type. This page serves as a reference for the expected configuration options
available in `ProtocolMapperRepresentation.config` for each `ProtocolMapper` implementation.

<#assign mappers = ctx.protocolMappers.getMappers() />
<#list mappers as protocol, categoryMap>

== ${protocol}

The following section contains all `ProtocolMapper` implementation associated with the ${protocol} protocol.

<#list categoryMap as category, mapperList>

=== ${category}

<#list mapperList as mapper>

==== ${mapper.displayType()}

${(mapper.helpText())!"_No description available._"}

ID:: `${mapper.id()}`
Implementation:: `${mapper.implementationClass()}`

<#if mapper.configProperties()?has_content>
.${mapper.displayType()} Configuration Properties
[cols="1,1,1,1,2",options="header"]
|===
|Name |Property |Type |Default |Description

<#list mapper.configProperties() as prop>
<#if prop.getName()??>
|${ctx.protocolMappers.resolveLabel(prop.getLabel()!prop.getName())}
|`${prop.getName()}`
|`${prop.getType()!"String"}`
|<#if prop.getDefaultValue()??>`${prop.getDefaultValue()?string}`<#else>_None_</#if>
|${ctx.protocolMappers.resolveTooltip((prop.getHelpText())!"")}

</#if>
</#list>
|===
</#if>

'''
</#list>
</#list>
</#list>

</@tmpl.guide>
7 changes: 7 additions & 0 deletions docs/guides/assembly.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,12 @@
<include>pinned-guides</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.basedir}/admin-api</directory>
<outputDirectory>generated-guides/admin-api/</outputDirectory>
<includes>
<include>pinned-guides</include>
</includes>
</fileSet>
</fileSets>
</assembly>
12 changes: 12 additions & 0 deletions docs/guides/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,18 @@
<preserveDirectories>true</preserveDirectories>
</configuration>
</execution>
<execution>
<id>admin-api-asciidoc-to-html</id>
<phase>generate-resources</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<sourceDirectory>${basedir}/target/generated-guides/admin-api</sourceDirectory>
<outputDirectory>${project.build.directory}/generated-docs/admin-api</outputDirectory>
<preserveDirectories>true</preserveDirectories>
</configuration>
</execution>
<execution>
<id>ui-customization-asciidoc-to-html</id>
<phase>generate-resources</phase>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ public class Context {

private final Options options;
private final Features features;
private final ProtocolMappers protocolMappers;
private final List<Guide> guides;

public Context(Path srcPath) throws IOException {
this.options = new Options();
this.features = new Features();
this.protocolMappers = new ProtocolMappers(srcPath.getParent().getParent().getParent());
this.guides = new LinkedList<>();

Path partials = srcPath.resolve("partials");
Expand Down Expand Up @@ -81,6 +83,10 @@ public Features getFeatures() {
return features;
}

public ProtocolMappers getProtocolMappers() {
return protocolMappers;
}

public List<Guide> getGuides() {
return guides;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.keycloak.guides.maven;

import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;

import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderManager;
import org.keycloak.quarkus.runtime.Providers;

public class ProtocolMappers {

private static final String MESSAGES_RELATIVE_PATH = "js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties";

private final Map<String, Map<String, List<ProtocolMapperInfo>>> mappers;
private final Properties messages;

public ProtocolMappers(Path projectRootDir) {
messages = loadMessages(projectRootDir);
ProviderManager providerManager = Providers.getProviderManager(Thread.currentThread().getContextClassLoader());

mappers = providerManager.loadSpis().stream()
.filter(spi -> spi.getName().equals("protocol-mapper"))
.findFirst()
.<Map<String, Map<String, List<ProtocolMapperInfo>>>>map(spi -> providerManager.load(spi).stream()
.map(ProtocolMapper.class::cast)
.sorted(Comparator.comparing(ProtocolMapper::getDisplayType))
.map(mapper -> new ProtocolMapperInfo(
mapper.getId(),
mapper.getClass().getName(),
mapper.getProtocol(),
mapper.getDisplayType(),
mapper.getDisplayCategory(),
mapper.getHelpText(),
mapper.getPriority(),
mapper.getConfigProperties()
))
.collect(Collectors.groupingBy(
ProtocolMapperInfo::protocol,
LinkedHashMap::new,
Collectors.groupingBy(
ProtocolMapperInfo::category,
LinkedHashMap::new,
Collectors.toList()
)
)))
.orElse(Map.of());
}

public Map<String, Map<String, List<ProtocolMapperInfo>>> getMappers() {
return mappers;
}

public String resolveLabel(String label) {
if (label != null && label.endsWith(".label")) {
return messages.getProperty(label, label);
}
return label;
}

public String resolveTooltip(String tooltip) {
if (tooltip != null && tooltip.endsWith(".tooltip")) {
return messages.getProperty(tooltip, tooltip);
}
return tooltip;
}

private static Properties loadMessages(Path projectRootDir) {
Properties props = new Properties();
Path messagesFile = projectRootDir.resolve(MESSAGES_RELATIVE_PATH);
if (Files.exists(messagesFile)) {
try (Reader reader = Files.newBufferedReader(messagesFile)) {
props.load(reader);
} catch (IOException e) {
throw new RuntimeException("Failed to load admin messages properties from " + messagesFile, e);
}
}
return props;
}

public record ProtocolMapperInfo(String id, String implementationClass, String protocol, String displayType,
String category, String helpText, int priority,
List<ProviderConfigProperty> configProperties) {

}
}
Loading