SAML javascript protocol mapper: disable uploading scripts through admin console by default (#14293)

Closes #14292
This commit is contained in:
Marek Posolda 2022-09-09 13:47:51 +02:00 committed by GitHub
parent 869ccc82b2
commit 040e52cfd7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 269 additions and 6 deletions

View file

@ -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
@ -54,6 +56,11 @@ public class ScriptProviderDescriptor {
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);
}
@ -76,4 +83,8 @@ public class ScriptProviderDescriptor {
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);
}
}

View file

@ -30,6 +30,7 @@ import static org.keycloak.representations.provider.ScriptProviderDescriptor.MAP
import static org.keycloak.representations.provider.ScriptProviderDescriptor.POLICIES;
import static org.keycloak.quarkus.runtime.Environment.getProviderFiles;
import static org.keycloak.theme.ClasspathThemeProviderFactory.KEYCLOAK_THEMES_JSON;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.SAML_MAPPERS;
import javax.persistence.Entity;
import javax.persistence.spi.PersistenceUnitTransactionType;
@ -92,6 +93,7 @@ import org.keycloak.config.StorageOptions;
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;
@ -174,6 +176,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) {
@ -188,6 +191,10 @@ class KeycloakProcessor {
return new DeployedScriptOIDCProtocolMapper(metadata);
}
private static ProviderFactory registerSAMLScriptMapper(ScriptProviderMetadata metadata) {
return new DeployedScriptSAMLProtocolMapper(metadata);
}
@BuildStep
FeatureBuildItem getFeature() {
return new FeatureBuildItem("keycloak");
@ -679,7 +686,7 @@ class KeycloakProcessor {
}
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;

View file

@ -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;
}
}

View file

@ -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;
@ -20,7 +22,7 @@ import java.util.*;
*
* @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";
@ -92,6 +94,11 @@ public class ScriptBasedMapper extends AbstractSAMLProtocolMapper implements SAM
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
@ -110,7 +117,7 @@ public class ScriptBasedMapper extends AbstractSAMLProtocolMapper implements SAM
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);
@ -158,7 +165,7 @@ public class ScriptBasedMapper extends AbstractSAMLProtocolMapper implements SAM
@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;
}
@ -173,6 +180,10 @@ public class ScriptBasedMapper extends AbstractSAMLProtocolMapper implements SAM
}
}
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.

View file

@ -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

View file

@ -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<>();

View file

@ -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);
}
}

View file

@ -40,6 +40,7 @@ import org.keycloak.models.RealmModel;
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;
@ -601,6 +602,9 @@ public class KeycloakServer {
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);

View file

@ -19,6 +19,7 @@ package org.keycloak.subsystem.server.extension;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.AUTHENTICATORS;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.MAPPERS;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.POLICIES;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.SAML_MAPPERS;
import java.io.IOException;
import java.io.InputStream;
@ -39,6 +40,7 @@ import org.keycloak.authorization.policy.provider.js.DeployedScriptPolicyFactory
import org.keycloak.common.util.StreamUtil;
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.representations.provider.ScriptProviderDescriptor;
import org.keycloak.representations.provider.ScriptProviderMetadata;
@ -63,6 +65,10 @@ final class ScriptProviderDeploymentProcessor {
info.addProvider(ProtocolMapperSpi.class, new DeployedScriptOIDCProtocolMapper(metadata));
}
private static void registerSAMLScriptMapper(KeycloakDeploymentInfo info, ScriptProviderMetadata metadata) {
info.addProvider(ProtocolMapperSpi.class, new DeployedScriptSAMLProtocolMapper(metadata));
}
static void deploy(DeploymentUnit deploymentUnit, KeycloakDeploymentInfo info) {
ResourceRoot resourceRoot = deploymentUnit.getAttachment(Attachments.DEPLOYMENT_ROOT);
@ -129,5 +135,6 @@ final class ScriptProviderDeploymentProcessor {
PROVIDERS.put(AUTHENTICATORS, ScriptProviderDeploymentProcessor::registerScriptAuthenticator);
PROVIDERS.put(POLICIES, ScriptProviderDeploymentProcessor::registerScriptPolicy);
PROVIDERS.put(MAPPERS, ScriptProviderDeploymentProcessor::registerScriptMapper);
PROVIDERS.put(SAML_MAPPERS, ScriptProviderDeploymentProcessor::registerSAMLScriptMapper);
}
}