Skip to content

Commit e2ae7ee

Browse files
authored
SAML javascript protocol mapper: disable uploading scripts through admin console by default (#14296)
Closes #14292 (cherry picked from commit 033a1d7)
1 parent a544bd8 commit e2ae7ee

File tree

9 files changed

+269
-6
lines changed

9 files changed

+269
-6
lines changed

core/src/main/java/org/keycloak/representations/provider/ScriptProviderDescriptor.java

+11
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public class ScriptProviderDescriptor {
3131
public static final String POLICIES = "policies";
3232
public static final String MAPPERS = "mappers";
3333

34+
public static final String SAML_MAPPERS = "saml-mappers";
35+
3436
private Map<String, List<ScriptProviderMetadata>> providers = new HashMap<>();
3537

3638
@JsonUnwrapped
@@ -54,6 +56,11 @@ public void setMappers(List<ScriptProviderMetadata> metadata) {
5456
providers.put(MAPPERS, metadata);
5557
}
5658

59+
@JsonSetter(SAML_MAPPERS)
60+
public void setSAMLMappers(List<ScriptProviderMetadata> metadata) {
61+
providers.put(SAML_MAPPERS, metadata);
62+
}
63+
5764
public void addAuthenticator(String name, String fileName) {
5865
addProvider(AUTHENTICATORS, name, fileName, null);
5966
}
@@ -76,4 +83,8 @@ public void addPolicy(String name, String fileName) {
7683
public void addMapper(String name, String fileName) {
7784
addProvider(MAPPERS, name, fileName, null);
7885
}
86+
87+
public void addSAMLMapper(String name, String fileName) {
88+
addProvider(SAML_MAPPERS, name, fileName, null);
89+
}
7990
}

quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import static org.keycloak.representations.provider.ScriptProviderDescriptor.MAPPERS;
3030
import static org.keycloak.representations.provider.ScriptProviderDescriptor.POLICIES;
3131
import static org.keycloak.quarkus.runtime.Environment.getProviderFiles;
32+
import static org.keycloak.representations.provider.ScriptProviderDescriptor.SAML_MAPPERS;
3233

3334
import javax.persistence.Entity;
3435
import javax.persistence.spi.PersistenceUnitTransactionType;
@@ -91,6 +92,7 @@
9192
import org.keycloak.connections.jpa.JpaConnectionProvider;
9293
import org.keycloak.connections.jpa.JpaConnectionSpi;
9394
import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory;
95+
import org.keycloak.protocol.saml.mappers.DeployedScriptSAMLProtocolMapper;
9496
import org.keycloak.quarkus.runtime.QuarkusProfile;
9597
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
9698
import org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource;
@@ -170,6 +172,7 @@ class KeycloakProcessor {
170172
DEPLOYEABLE_SCRIPT_PROVIDERS.put(AUTHENTICATORS, KeycloakProcessor::registerScriptAuthenticator);
171173
DEPLOYEABLE_SCRIPT_PROVIDERS.put(POLICIES, KeycloakProcessor::registerScriptPolicy);
172174
DEPLOYEABLE_SCRIPT_PROVIDERS.put(MAPPERS, KeycloakProcessor::registerScriptMapper);
175+
DEPLOYEABLE_SCRIPT_PROVIDERS.put(SAML_MAPPERS, KeycloakProcessor::registerSAMLScriptMapper);
173176
}
174177

175178
private static ProviderFactory registerScriptAuthenticator(ScriptProviderMetadata metadata) {
@@ -184,6 +187,10 @@ private static ProviderFactory registerScriptMapper(ScriptProviderMetadata metad
184187
return new DeployedScriptOIDCProtocolMapper(metadata);
185188
}
186189

190+
private static ProviderFactory registerSAMLScriptMapper(ScriptProviderMetadata metadata) {
191+
return new DeployedScriptSAMLProtocolMapper(metadata);
192+
}
193+
187194
@BuildStep
188195
FeatureBuildItem getFeature() {
189196
return new FeatureBuildItem("keycloak");
@@ -660,7 +667,7 @@ private ProviderFactory createDeployableScriptProvider(JarFile jarFile, Entry<St
660667
}
661668

662669
private boolean isScriptForSpi(Spi spi, String type) {
663-
if (spi instanceof ProtocolMapperSpi && MAPPERS.equals(type)) {
670+
if (spi instanceof ProtocolMapperSpi && (MAPPERS.equals(type) || SAML_MAPPERS.equals(type))) {
664671
return true;
665672
} else if (spi instanceof PolicySpi && POLICIES.equals(type)) {
666673
return true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package org.keycloak.protocol.saml.mappers;
2+
3+
import java.util.List;
4+
import java.util.stream.Collectors;
5+
6+
import org.keycloak.common.Profile;
7+
import org.keycloak.models.ProtocolMapperModel;
8+
import org.keycloak.provider.ProviderConfigProperty;
9+
import org.keycloak.representations.provider.ScriptProviderMetadata;
10+
11+
/**
12+
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
13+
*/
14+
public class DeployedScriptSAMLProtocolMapper extends ScriptBasedMapper {
15+
16+
protected ScriptProviderMetadata metadata;
17+
18+
public DeployedScriptSAMLProtocolMapper(ScriptProviderMetadata metadata) {
19+
this.metadata = metadata;
20+
}
21+
22+
public DeployedScriptSAMLProtocolMapper() {
23+
// for reflection
24+
}
25+
26+
@Override
27+
public String getId() {
28+
return metadata.getId();
29+
}
30+
31+
@Override
32+
public String getDisplayType() {
33+
return metadata.getName();
34+
}
35+
36+
@Override
37+
public String getHelpText() {
38+
return metadata.getDescription();
39+
}
40+
41+
@Override
42+
protected String getScriptCode(ProtocolMapperModel mapperModel) {
43+
return metadata.getCode();
44+
}
45+
46+
public List<ProviderConfigProperty> getConfigProperties() {
47+
return super.getConfigProperties().stream()
48+
.filter(providerConfigProperty -> !ProviderConfigProperty.SCRIPT_TYPE.equals(providerConfigProperty.getName())) // filter "script" property
49+
.collect(Collectors.toList());
50+
}
51+
52+
public void setMetadata(ScriptProviderMetadata metadata) {
53+
this.metadata = metadata;
54+
}
55+
56+
public ScriptProviderMetadata getMetadata() {
57+
return metadata;
58+
}
59+
}

services/src/main/java/org/keycloak/protocol/saml/mappers/ScriptBasedMapper.java

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package org.keycloak.protocol.saml.mappers;
22

33
import org.jboss.logging.Logger;
4+
import org.keycloak.common.Profile;
45
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
56
import org.keycloak.dom.saml.v2.assertion.AttributeType;
67
import org.keycloak.models.*;
78
import org.keycloak.protocol.ProtocolMapperConfigException;
9+
import org.keycloak.provider.EnvironmentDependentProviderFactory;
810
import org.keycloak.provider.ProviderConfigProperty;
911
import org.keycloak.scripting.EvaluatableScriptAdapter;
1012
import org.keycloak.scripting.ScriptCompilationException;
@@ -20,7 +22,7 @@
2022
*
2123
* @author Alistair Doswald
2224
*/
23-
public class ScriptBasedMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper {
25+
public class ScriptBasedMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper, EnvironmentDependentProviderFactory {
2426

2527
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
2628
public static final String PROVIDER_ID = "saml-javascript-mapper";
@@ -92,6 +94,11 @@ public String getHelpText() {
9294
return "Evaluates a JavaScript function to produce an attribute value based on context information.";
9395
}
9496

97+
@Override
98+
public boolean isSupported() {
99+
return Profile.isFeatureEnabled(Profile.Feature.SCRIPTS);
100+
}
101+
95102
/**
96103
* This method attaches one or many attributes to the passed attribute statement.
97104
* To obtain the attribute values, it executes the mapper's script and returns attaches the returned value to the
@@ -110,7 +117,7 @@ public void transformAttributeStatement(AttributeStatementType attributeStatemen
110117
KeycloakSession session, UserSessionModel userSession,
111118
AuthenticatedClientSessionModel clientSession) {
112119
UserModel user = userSession.getUser();
113-
String scriptSource = mappingModel.getConfig().get(ProviderConfigProperty.SCRIPT_TYPE);
120+
String scriptSource = getScriptCode(mappingModel);
114121
RealmModel realm = userSession.getRealm();
115122

116123
String single = mappingModel.getConfig().get(SINGLE_VALUE_ATTRIBUTE);
@@ -158,7 +165,7 @@ public void transformAttributeStatement(AttributeStatementType attributeStatemen
158165
@Override
159166
public void validateConfig(KeycloakSession session, RealmModel realm, ProtocolMapperContainerModel client, ProtocolMapperModel mapperModel) throws ProtocolMapperConfigException {
160167

161-
String scriptCode = mapperModel.getConfig().get(ProviderConfigProperty.SCRIPT_TYPE);
168+
String scriptCode = getScriptCode(mapperModel);
162169
if (scriptCode == null) {
163170
return;
164171
}
@@ -173,6 +180,10 @@ public void validateConfig(KeycloakSession session, RealmModel realm, ProtocolMa
173180
}
174181
}
175182

183+
protected String getScriptCode(ProtocolMapperModel mappingModel) {
184+
return mappingModel.getConfig().get(ProviderConfigProperty.SCRIPT_TYPE);
185+
}
186+
176187
/**
177188
* Creates an protocol mapper model for the this script based mapper. This mapper model is meant to be used for
178189
* testing, as normally such objects are created in a different manner through the keycloak GUI.

services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper

-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ org.keycloak.protocol.saml.mappers.UserAttributeStatementMapper
3636
org.keycloak.protocol.saml.mappers.UserPropertyAttributeStatementMapper
3737
org.keycloak.protocol.saml.mappers.UserSessionNoteStatementMapper
3838
org.keycloak.protocol.saml.mappers.GroupMembershipMapper
39-
org.keycloak.protocol.saml.mappers.ScriptBasedMapper
4039
org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper
4140
org.keycloak.protocol.oidc.mappers.UserRealmRoleMappingMapper
4241
org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper

testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/TestCleanup.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public class TestCleanup {
5252
private final String realmName;
5353
private final ConcurrentLinkedDeque<Runnable> genericCleanups = new ConcurrentLinkedDeque<>();
5454

55-
// Key is kind of entity (eg. "client", "role", "user" etc), Values are all kind of entities of given type to cleanup
55+
// Key is kind of entity (eg. "client", "role", "user" etc), Values are all IDs of entities of given type to cleanup
5656
private final ConcurrentMultivaluedHashMap<String, String> entities = new ConcurrentMultivaluedHashMap<>();
5757

5858

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package org.keycloak.testsuite.script;
2+
3+
import java.io.IOException;
4+
import java.util.Collections;
5+
import java.util.stream.Stream;
6+
7+
import javax.ws.rs.core.Response;
8+
9+
import org.jboss.arquillian.container.test.api.Deployer;
10+
import org.jboss.arquillian.container.test.api.Deployment;
11+
import org.jboss.arquillian.container.test.api.TargetsContainer;
12+
import org.jboss.arquillian.test.api.ArquillianResource;
13+
import org.jboss.shrinkwrap.api.ShrinkWrap;
14+
import org.jboss.shrinkwrap.api.asset.StringAsset;
15+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
16+
import org.junit.After;
17+
import org.junit.Assert;
18+
import org.junit.Before;
19+
import org.junit.BeforeClass;
20+
import org.junit.Test;
21+
import org.keycloak.common.Profile;
22+
import org.keycloak.dom.saml.v2.assertion.AssertionType;
23+
import org.keycloak.dom.saml.v2.assertion.AttributeType;
24+
import org.keycloak.protocol.saml.SamlProtocol;
25+
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
26+
import org.keycloak.protocol.saml.mappers.ScriptBasedMapper;
27+
import org.keycloak.provider.ProviderConfigProperty;
28+
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
29+
import org.keycloak.representations.provider.ScriptProviderDescriptor;
30+
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
31+
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
32+
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
33+
import org.keycloak.testsuite.arquillian.annotation.EnableFeatures;
34+
import org.keycloak.testsuite.saml.AbstractSamlTest;
35+
import org.keycloak.testsuite.saml.RoleMapperTest;
36+
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
37+
import org.keycloak.testsuite.updaters.ProtocolMappersUpdater;
38+
import org.keycloak.testsuite.util.ContainerAssume;
39+
import org.keycloak.testsuite.util.Matchers;
40+
import org.keycloak.testsuite.util.SamlClient;
41+
import org.keycloak.testsuite.util.SamlClientBuilder;
42+
import org.keycloak.util.JsonSerialization;
43+
44+
import static org.junit.Assert.assertFalse;
45+
import static org.junit.Assert.assertThat;
46+
import static org.keycloak.common.Profile.Feature.SCRIPTS;
47+
import static org.keycloak.testsuite.arquillian.DeploymentTargetModifier.AUTH_SERVER_CURRENT;
48+
import static org.keycloak.testsuite.saml.RoleMapperTest.createSamlProtocolMapper;
49+
import static org.keycloak.testsuite.util.SamlStreams.assertionsUnencrypted;
50+
import static org.keycloak.testsuite.util.SamlStreams.attributeStatements;
51+
import static org.keycloak.testsuite.util.SamlStreams.attributesUnecrypted;
52+
53+
/**
54+
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
55+
*/
56+
public class DeployedSAMLScriptMapperTest extends AbstractSamlTest {
57+
58+
private static final String SCRIPT_DEPLOYMENT_NAME = "scripts.jar";
59+
60+
private ClientAttributeUpdater cau;
61+
private ProtocolMappersUpdater pmu;
62+
63+
@Deployment(name = SCRIPT_DEPLOYMENT_NAME, managed = false, testable = false)
64+
@TargetsContainer(AUTH_SERVER_CURRENT)
65+
public static JavaArchive deploy() throws IOException {
66+
ScriptProviderDescriptor representation = new ScriptProviderDescriptor();
67+
68+
representation.addSAMLMapper("My Mapper", "mapper-a.js");
69+
70+
return ShrinkWrap.create(JavaArchive.class, SCRIPT_DEPLOYMENT_NAME)
71+
.addAsManifestResource(new StringAsset(JsonSerialization.writeValueAsPrettyString(representation)),
72+
"keycloak-scripts.json")
73+
.addAsResource("scripts/mapper-example.js", "mapper-a.js");
74+
}
75+
76+
@BeforeClass
77+
public static void verifyEnvironment() {
78+
ContainerAssume.assumeNotAuthServerUndertow();
79+
}
80+
81+
@ArquillianResource
82+
private Deployer deployer;
83+
84+
@Before
85+
public void deployScripts() throws Exception {
86+
deployer.deploy(SCRIPT_DEPLOYMENT_NAME);
87+
reconnectAdminClient();
88+
}
89+
90+
@Before
91+
public void cleanMappersAndScopes() {
92+
this.cau = ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_EMPLOYEE_2)
93+
.setDefaultClientScopes(Collections.EMPTY_LIST)
94+
.update();
95+
this.pmu = cau.protocolMappers()
96+
.clear()
97+
.update();
98+
99+
getCleanup(REALM_NAME)
100+
.addCleanup(this.cau)
101+
.addCleanup(this.pmu);
102+
}
103+
104+
@After
105+
public void onAfter() throws Exception {
106+
deployer.undeploy(SCRIPT_DEPLOYMENT_NAME);
107+
reconnectAdminClient();
108+
}
109+
110+
@Test
111+
public void testScriptMapperNotAvailableThroughAdminRest() {
112+
assertFalse(adminClient.serverInfo().getInfo().getProtocolMapperTypes().get(SamlProtocol.LOGIN_PROTOCOL).stream()
113+
.anyMatch(
114+
mapper -> ScriptBasedMapper.PROVIDER_ID.equals(mapper.getId())));
115+
116+
// Doublecheck not possible to create mapper through admin REST
117+
ProtocolMapperRepresentation mapperRep = createSamlProtocolMapper(ScriptBasedMapper.PROVIDER_ID,
118+
ProviderConfigProperty.SCRIPT_TYPE, "'hello_' + user.username",
119+
AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, AttributeStatementHelper.BASIC,
120+
AttributeStatementHelper.SAML_ATTRIBUTE_NAME, "SCRIPT_ATTRIBUTE"
121+
);
122+
123+
Response response = pmu.getResource().createMapper(mapperRep);
124+
Assert.assertEquals(404, response.getStatus());
125+
response.close();
126+
}
127+
128+
129+
@Test
130+
@EnableFeature(value = SCRIPTS, skipRestart = true, executeAsLast = false)
131+
public void testScriptMappingThroughServerDeploy() {
132+
// ScriptBasedMapper still not available even if SCRIPTS feature is enabled
133+
testScriptMapperNotAvailableThroughAdminRest();
134+
135+
pmu.add(
136+
createSamlProtocolMapper("script-mapper-a.js",
137+
AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, AttributeStatementHelper.BASIC,
138+
AttributeStatementHelper.SAML_ATTRIBUTE_NAME, "SCRIPT_ATTRIBUTE"
139+
)
140+
).update();
141+
142+
assertLoginSuccessWithAttributeAvailable();
143+
}
144+
145+
146+
private void assertLoginSuccessWithAttributeAvailable() {
147+
SAMLDocumentHolder samlResponse = new SamlClientBuilder()
148+
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_EMPLOYEE_2, RoleMapperTest.SAML_ASSERTION_CONSUMER_URL_EMPLOYEE_2, SamlClient.Binding.POST)
149+
.build()
150+
.login().user(bburkeUser).build()
151+
.getSamlResponse(SamlClient.Binding.POST);
152+
153+
assertThat(samlResponse.getSamlObject(), Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
154+
155+
Stream<AssertionType> assertions = assertionsUnencrypted(samlResponse.getSamlObject());
156+
Stream<AttributeType> attributes = attributesUnecrypted(attributeStatements(assertions));
157+
String scriptAttrValue = attributes
158+
.filter(attribute -> "SCRIPT_ATTRIBUTE".equals(attribute.getName()))
159+
.map(attribute -> attribute.getAttributeValue().get(0).toString())
160+
.findFirst().orElseThrow(() -> new AssertionError("Attribute SCRIPT_ATTRIBUTE was not available in SAML assertion"));
161+
162+
Assert.assertEquals("hello_bburke", scriptAttrValue);
163+
}
164+
165+
}

testsuite/utils/src/main/java/org/keycloak/testsuite/KeycloakServer.java

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.keycloak.platform.Platform;
4141
import org.keycloak.protocol.ProtocolMapperSpi;
4242
import org.keycloak.protocol.oidc.mappers.DeployedScriptOIDCProtocolMapper;
43+
import org.keycloak.protocol.saml.mappers.DeployedScriptSAMLProtocolMapper;
4344
import org.keycloak.provider.KeycloakDeploymentInfo;
4445
import org.keycloak.provider.ProviderFactory;
4546
import org.keycloak.provider.ProviderManager;
@@ -601,6 +602,9 @@ public static void registerScriptProviders(DefaultKeycloakSessionFactory session
601602
addScriptProvider(info, scriptProviderDescriptor.getProviders().getOrDefault("mappers", Collections.emptyList()),
602603
ProtocolMapperSpi.class,
603604
DeployedScriptOIDCProtocolMapper::new);
605+
addScriptProvider(info, scriptProviderDescriptor.getProviders().getOrDefault("saml-mappers", Collections.emptyList()),
606+
ProtocolMapperSpi.class,
607+
DeployedScriptSAMLProtocolMapper::new);
604608
addScriptProvider(info, scriptProviderDescriptor.getProviders().getOrDefault("policies", Collections.emptyList()),
605609
PolicySpi.class,
606610
DeployedScriptPolicyFactory::new);

0 commit comments

Comments
 (0)