Skip to content

Commit

Permalink
SAML javascript protocol mapper: disable uploading scripts through ad…
Browse files Browse the repository at this point in the history
…min console by default (#14296)

Closes #14292

(cherry picked from commit 033a1d7)
  • Loading branch information
mposolda committed Sep 9, 2022
1 parent a544bd8 commit e2ae7ee
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public class ScriptProviderDescriptor {
public static final String POLICIES = "policies";
public static final String MAPPERS = "mappers";

public static final String SAML_MAPPERS = "saml-mappers";

private Map<String, List<ScriptProviderMetadata>> providers = new HashMap<>();

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

@JsonSetter(SAML_MAPPERS)
public void setSAMLMappers(List<ScriptProviderMetadata> metadata) {
providers.put(SAML_MAPPERS, metadata);
}

public void addAuthenticator(String name, String fileName) {
addProvider(AUTHENTICATORS, name, fileName, null);
}
Expand All @@ -76,4 +83,8 @@ public void addPolicy(String name, String fileName) {
public void addMapper(String name, String fileName) {
addProvider(MAPPERS, name, fileName, null);
}

public void addSAMLMapper(String name, String fileName) {
addProvider(SAML_MAPPERS, name, fileName, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import static org.keycloak.representations.provider.ScriptProviderDescriptor.MAPPERS;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.POLICIES;
import static org.keycloak.quarkus.runtime.Environment.getProviderFiles;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.SAML_MAPPERS;

import javax.persistence.Entity;
import javax.persistence.spi.PersistenceUnitTransactionType;
Expand Down Expand Up @@ -91,6 +92,7 @@
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.connections.jpa.JpaConnectionSpi;
import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory;
import org.keycloak.protocol.saml.mappers.DeployedScriptSAMLProtocolMapper;
import org.keycloak.quarkus.runtime.QuarkusProfile;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource;
Expand Down Expand Up @@ -170,6 +172,7 @@ class KeycloakProcessor {
DEPLOYEABLE_SCRIPT_PROVIDERS.put(AUTHENTICATORS, KeycloakProcessor::registerScriptAuthenticator);
DEPLOYEABLE_SCRIPT_PROVIDERS.put(POLICIES, KeycloakProcessor::registerScriptPolicy);
DEPLOYEABLE_SCRIPT_PROVIDERS.put(MAPPERS, KeycloakProcessor::registerScriptMapper);
DEPLOYEABLE_SCRIPT_PROVIDERS.put(SAML_MAPPERS, KeycloakProcessor::registerSAMLScriptMapper);
}

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

private static ProviderFactory registerSAMLScriptMapper(ScriptProviderMetadata metadata) {
return new DeployedScriptSAMLProtocolMapper(metadata);
}

@BuildStep
FeatureBuildItem getFeature() {
return new FeatureBuildItem("keycloak");
Expand Down Expand Up @@ -660,7 +667,7 @@ private ProviderFactory createDeployableScriptProvider(JarFile jarFile, Entry<St
}

private boolean isScriptForSpi(Spi spi, String type) {
if (spi instanceof ProtocolMapperSpi && MAPPERS.equals(type)) {
if (spi instanceof ProtocolMapperSpi && (MAPPERS.equals(type) || SAML_MAPPERS.equals(type))) {
return true;
} else if (spi instanceof PolicySpi && POLICIES.equals(type)) {
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.keycloak.protocol.saml.mappers;

import java.util.List;
import java.util.stream.Collectors;

import org.keycloak.common.Profile;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.provider.ScriptProviderMetadata;

/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DeployedScriptSAMLProtocolMapper extends ScriptBasedMapper {

protected ScriptProviderMetadata metadata;

public DeployedScriptSAMLProtocolMapper(ScriptProviderMetadata metadata) {
this.metadata = metadata;
}

public DeployedScriptSAMLProtocolMapper() {
// for reflection
}

@Override
public String getId() {
return metadata.getId();
}

@Override
public String getDisplayType() {
return metadata.getName();
}

@Override
public String getHelpText() {
return metadata.getDescription();
}

@Override
protected String getScriptCode(ProtocolMapperModel mapperModel) {
return metadata.getCode();
}

public List<ProviderConfigProperty> getConfigProperties() {
return super.getConfigProperties().stream()
.filter(providerConfigProperty -> !ProviderConfigProperty.SCRIPT_TYPE.equals(providerConfigProperty.getName())) // filter "script" property
.collect(Collectors.toList());
}

public void setMetadata(ScriptProviderMetadata metadata) {
this.metadata = metadata;
}

public ScriptProviderMetadata getMetadata() {
return metadata;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package org.keycloak.protocol.saml.mappers;

import org.jboss.logging.Logger;
import org.keycloak.common.Profile;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.assertion.AttributeType;
import org.keycloak.models.*;
import org.keycloak.protocol.ProtocolMapperConfigException;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.scripting.EvaluatableScriptAdapter;
import org.keycloak.scripting.ScriptCompilationException;
Expand All @@ -20,7 +22,7 @@
*
* @author Alistair Doswald
*/
public class ScriptBasedMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper {
public class ScriptBasedMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper, EnvironmentDependentProviderFactory {

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

@Override
public boolean isSupported() {
return Profile.isFeatureEnabled(Profile.Feature.SCRIPTS);
}

/**
* This method attaches one or many attributes to the passed attribute statement.
* To obtain the attribute values, it executes the mapper's script and returns attaches the returned value to the
Expand All @@ -110,7 +117,7 @@ public void transformAttributeStatement(AttributeStatementType attributeStatemen
KeycloakSession session, UserSessionModel userSession,
AuthenticatedClientSessionModel clientSession) {
UserModel user = userSession.getUser();
String scriptSource = mappingModel.getConfig().get(ProviderConfigProperty.SCRIPT_TYPE);
String scriptSource = getScriptCode(mappingModel);
RealmModel realm = userSession.getRealm();

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

String scriptCode = mapperModel.getConfig().get(ProviderConfigProperty.SCRIPT_TYPE);
String scriptCode = getScriptCode(mapperModel);
if (scriptCode == null) {
return;
}
Expand All @@ -173,6 +180,10 @@ public void validateConfig(KeycloakSession session, RealmModel realm, ProtocolMa
}
}

protected String getScriptCode(ProtocolMapperModel mappingModel) {
return mappingModel.getConfig().get(ProviderConfigProperty.SCRIPT_TYPE);
}

/**
* Creates an protocol mapper model for the this script based mapper. This mapper model is meant to be used for
* testing, as normally such objects are created in a different manner through the keycloak GUI.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ org.keycloak.protocol.saml.mappers.UserAttributeStatementMapper
org.keycloak.protocol.saml.mappers.UserPropertyAttributeStatementMapper
org.keycloak.protocol.saml.mappers.UserSessionNoteStatementMapper
org.keycloak.protocol.saml.mappers.GroupMembershipMapper
org.keycloak.protocol.saml.mappers.ScriptBasedMapper
org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper
org.keycloak.protocol.oidc.mappers.UserRealmRoleMappingMapper
org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public class TestCleanup {
private final String realmName;
private final ConcurrentLinkedDeque<Runnable> genericCleanups = new ConcurrentLinkedDeque<>();

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


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package org.keycloak.testsuite.script;

import java.io.IOException;
import java.util.Collections;
import java.util.stream.Stream;

import javax.ws.rs.core.Response;

import org.jboss.arquillian.container.test.api.Deployer;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.TargetsContainer;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.keycloak.common.Profile;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AttributeType;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
import org.keycloak.protocol.saml.mappers.ScriptBasedMapper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.provider.ScriptProviderDescriptor;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.arquillian.annotation.EnableFeatures;
import org.keycloak.testsuite.saml.AbstractSamlTest;
import org.keycloak.testsuite.saml.RoleMapperTest;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.updaters.ProtocolMappersUpdater;
import org.keycloak.testsuite.util.ContainerAssume;
import org.keycloak.testsuite.util.Matchers;
import org.keycloak.testsuite.util.SamlClient;
import org.keycloak.testsuite.util.SamlClientBuilder;
import org.keycloak.util.JsonSerialization;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.keycloak.common.Profile.Feature.SCRIPTS;
import static org.keycloak.testsuite.arquillian.DeploymentTargetModifier.AUTH_SERVER_CURRENT;
import static org.keycloak.testsuite.saml.RoleMapperTest.createSamlProtocolMapper;
import static org.keycloak.testsuite.util.SamlStreams.assertionsUnencrypted;
import static org.keycloak.testsuite.util.SamlStreams.attributeStatements;
import static org.keycloak.testsuite.util.SamlStreams.attributesUnecrypted;

/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DeployedSAMLScriptMapperTest extends AbstractSamlTest {

private static final String SCRIPT_DEPLOYMENT_NAME = "scripts.jar";

private ClientAttributeUpdater cau;
private ProtocolMappersUpdater pmu;

@Deployment(name = SCRIPT_DEPLOYMENT_NAME, managed = false, testable = false)
@TargetsContainer(AUTH_SERVER_CURRENT)
public static JavaArchive deploy() throws IOException {
ScriptProviderDescriptor representation = new ScriptProviderDescriptor();

representation.addSAMLMapper("My Mapper", "mapper-a.js");

return ShrinkWrap.create(JavaArchive.class, SCRIPT_DEPLOYMENT_NAME)
.addAsManifestResource(new StringAsset(JsonSerialization.writeValueAsPrettyString(representation)),
"keycloak-scripts.json")
.addAsResource("scripts/mapper-example.js", "mapper-a.js");
}

@BeforeClass
public static void verifyEnvironment() {
ContainerAssume.assumeNotAuthServerUndertow();
}

@ArquillianResource
private Deployer deployer;

@Before
public void deployScripts() throws Exception {
deployer.deploy(SCRIPT_DEPLOYMENT_NAME);
reconnectAdminClient();
}

@Before
public void cleanMappersAndScopes() {
this.cau = ClientAttributeUpdater.forClient(adminClient, REALM_NAME, SAML_CLIENT_ID_EMPLOYEE_2)
.setDefaultClientScopes(Collections.EMPTY_LIST)
.update();
this.pmu = cau.protocolMappers()
.clear()
.update();

getCleanup(REALM_NAME)
.addCleanup(this.cau)
.addCleanup(this.pmu);
}

@After
public void onAfter() throws Exception {
deployer.undeploy(SCRIPT_DEPLOYMENT_NAME);
reconnectAdminClient();
}

@Test
public void testScriptMapperNotAvailableThroughAdminRest() {
assertFalse(adminClient.serverInfo().getInfo().getProtocolMapperTypes().get(SamlProtocol.LOGIN_PROTOCOL).stream()
.anyMatch(
mapper -> ScriptBasedMapper.PROVIDER_ID.equals(mapper.getId())));

// Doublecheck not possible to create mapper through admin REST
ProtocolMapperRepresentation mapperRep = createSamlProtocolMapper(ScriptBasedMapper.PROVIDER_ID,
ProviderConfigProperty.SCRIPT_TYPE, "'hello_' + user.username",
AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, AttributeStatementHelper.BASIC,
AttributeStatementHelper.SAML_ATTRIBUTE_NAME, "SCRIPT_ATTRIBUTE"
);

Response response = pmu.getResource().createMapper(mapperRep);
Assert.assertEquals(404, response.getStatus());
response.close();
}


@Test
@EnableFeature(value = SCRIPTS, skipRestart = true, executeAsLast = false)
public void testScriptMappingThroughServerDeploy() {
// ScriptBasedMapper still not available even if SCRIPTS feature is enabled
testScriptMapperNotAvailableThroughAdminRest();

pmu.add(
createSamlProtocolMapper("script-mapper-a.js",
AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, AttributeStatementHelper.BASIC,
AttributeStatementHelper.SAML_ATTRIBUTE_NAME, "SCRIPT_ATTRIBUTE"
)
).update();

assertLoginSuccessWithAttributeAvailable();
}


private void assertLoginSuccessWithAttributeAvailable() {
SAMLDocumentHolder samlResponse = new SamlClientBuilder()
.authnRequest(getAuthServerSamlEndpoint(REALM_NAME), SAML_CLIENT_ID_EMPLOYEE_2, RoleMapperTest.SAML_ASSERTION_CONSUMER_URL_EMPLOYEE_2, SamlClient.Binding.POST)
.build()
.login().user(bburkeUser).build()
.getSamlResponse(SamlClient.Binding.POST);

assertThat(samlResponse.getSamlObject(), Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));

Stream<AssertionType> assertions = assertionsUnencrypted(samlResponse.getSamlObject());
Stream<AttributeType> attributes = attributesUnecrypted(attributeStatements(assertions));
String scriptAttrValue = attributes
.filter(attribute -> "SCRIPT_ATTRIBUTE".equals(attribute.getName()))
.map(attribute -> attribute.getAttributeValue().get(0).toString())
.findFirst().orElseThrow(() -> new AssertionError("Attribute SCRIPT_ATTRIBUTE was not available in SAML assertion"));

Assert.assertEquals("hello_bburke", scriptAttrValue);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.keycloak.platform.Platform;
import org.keycloak.protocol.ProtocolMapperSpi;
import org.keycloak.protocol.oidc.mappers.DeployedScriptOIDCProtocolMapper;
import org.keycloak.protocol.saml.mappers.DeployedScriptSAMLProtocolMapper;
import org.keycloak.provider.KeycloakDeploymentInfo;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.ProviderManager;
Expand Down Expand Up @@ -601,6 +602,9 @@ public static void registerScriptProviders(DefaultKeycloakSessionFactory session
addScriptProvider(info, scriptProviderDescriptor.getProviders().getOrDefault("mappers", Collections.emptyList()),
ProtocolMapperSpi.class,
DeployedScriptOIDCProtocolMapper::new);
addScriptProvider(info, scriptProviderDescriptor.getProviders().getOrDefault("saml-mappers", Collections.emptyList()),
ProtocolMapperSpi.class,
DeployedScriptSAMLProtocolMapper::new);
addScriptProvider(info, scriptProviderDescriptor.getProviders().getOrDefault("policies", Collections.emptyList()),
PolicySpi.class,
DeployedScriptPolicyFactory::new);
Expand Down
Loading

0 comments on commit e2ae7ee

Please sign in to comment.