8000 Document provided ProtocolMapper implementations by ryanemerson · Pull Request #47331 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
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
120 changes: 120 additions & 0 deletions docs/guides/admin-api/protocol-mappers.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<#import "/templates/guide.adoc" as tmpl>

<@tmpl.guide
title="Protocol Mappers"
summary="Discover all built-in protocol mappers and how to use these to define token claims and assertion attributes.">

Protocol mappers provide a flexible way to define claims used in OAuth 2.0 tokens and endpoints, and attributes in SAML 2.0 assertions. For example adding user attributes or role mappings.

This page includes a list of all built-in protocol mappers, but {project_name} also supports defining custom protocol mappers through the `ProtocolMapper` SPI.

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. Th 8000 is page serves as a reference for the expected configuration options
available in `ProtocolMapperRepresentation.config` for each `ProtocolMapper` implementation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to include an example here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to include two examples 1) raw JSON payload 2) Java admin client ProtocolMapperRepresentation.

For example, to create a protocol mapper that maps a user attribute into an OIDC token claim, you would send a `ProtocolMapperRepresentation` using the following JSON:

[source,json]
----
{
"name": "my-user-attribute-mapper",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "phone_number", 8000
"claim.name": "phone",
"jsonType.label": "String"
}
}
----

The `protocolMapper` field corresponds to the "ID" listed in the tables below, and the `config` entries are described in each mapper's configuration table.

The same mapper can be created programmatically using the {project_name} Java admin client:

[source,java]
----
ProtocolMapperRepresentation mapper = new ProtocolMapperRepresentation();
mapper.setName("my-user-attribute-mapper");
mapper.setProtocol("openid-connect");
mapper.setProtocolMapper("oidc-usermodel-attribute-mapper");
mapper.setConfig(Map.of(
"user.attribute", "phone_number",
"claim.name", "phone",
"jsonType.label", "String"
));

Keycloak.getInstance(...)
.realm("my-realm")
.clientScopes()
.get("my-client-scope-id")
.getProtocolMappers()
.createMapper(mapper);
----

[discrete]
== Overview

The below table is an index of all available `ProtocolMapper` implementations provided by {project_name},
grouped by the associated protocol.


[cols="1,2",options="header"]
|===
|ID |Description
<#list ctx.protocolMappers.getMappersByProtocol(["openid-connect", "oid4vc", "saml", "docker-v2"]) as protocol, categoryMap>

2+a|**${protocol}**
<#list categoryMap as category, mapperList>
<#list mapperList as mapper>
|<<${mapper.id()},${mapper.id()}>>
|${(mapper.helpText())!"_No description available._"}
</#list>
</#list>
</#list>

|===

<#assign mappers = ctx.protocolMappers.getMappersByProtocol(["openid-connect", "oid4vc", "saml", "docker-v2"]) />
<#list mappers as protocol, categoryMap>

== ${protocol}

The following section contains all `ProtocolMapper` implementations associated with the ${protocol} protocol. For each
implementation we provide the "ID" of the `ProtocolMapper` and a table describing the supported configuration properties.

<#list categoryMap as category, mapperList>
<#list mapperList as mapper>

[#${mapper.id()}]
=== ${mapper.displayType()}

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

*ID*: `${mapper.id()}`

<#if mapper.configProperties()?has_content>
[cols="1,1,1,1,2",options="header"]
Comment thread
stianst marked this conversation as resolved.
Resolved
|===
|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,101 @@
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>>> getMappersByProtocol(List<String> protocols) {
Map<String, Map<String, List<ProtocolMapperInfo>>> ordered = new LinkedHashMap<>();
for (String protocol : protocols) {
Map<String, List<ProtocolMapperInfo>> categoryMap = mappers.get(protocol);
if (categoryMap != null) {
ordered.put(protocol, categoryMap);
}
}
return ordered;
}

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
0