From bb4ff5522999df626ae6f6b10060198377437926 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Wed, 21 Aug 2019 11:41:02 -0300 Subject: [PATCH] [KEYCLOAK-10868] - Deploy JavaScript code directly to Keycloak server Conflicts: testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java (cherry picked from commit 338fe2ae47a1494e786030eb39f908c964ea76c4) --- .../js/DeployedScriptPolicyFactory.java | 84 +++++++ .../provider/js/JSPolicyProviderFactory.java | 23 +- .../AbstractPermissionProvider.java | 5 + .../java/org/keycloak/common/Profile.java | 5 + .../authorization/JSPolicyRepresentation.java | 5 +- .../provider/ScriptProviderDescriptor.java | 79 +++++++ .../provider/ScriptProviderMetadata.java | 73 ++++++ .../main/module.xml | 4 + .../authorization/AuthorizationProvider.java | 13 +- .../provider/KeycloakDeploymentInfo.java | 16 +- .../DeployedScriptAuthenticatorFactory.java | 85 +++++++ .../browser/ScriptBasedAuthenticator.java | 17 +- .../ScriptBasedAuthenticatorFactory.java | 2 +- .../DefaultAuthorizationProviderFactory.java | 28 +-- .../admin/PolicyResourceService.java | 4 +- .../admin/ResourceServerService.java | 2 + .../DeployedScriptOIDCProtocolMapper.java | 80 +++++++ .../ScriptBasedOIDCProtocolMapper.java | 15 +- .../provider/DeploymentProviderLoader.java | 43 ++++ .../keycloak/provider/ProviderManager.java | 1 + .../org/keycloak/testsuite/ProfileAssume.java | 1 + .../testsuite/AbstractKeycloakTest.java | 21 ++ .../AbstractBasePhotozExampleAdapterTest.java | 2 + .../AbstractBaseServletAuthzAdapterTest.java | 9 +- .../ServletPolicyEnforcerTest.java | 4 + .../example/cors/CorsExampleAdapterTest.java | 2 + .../testsuite/admin/PermissionsTest.java | 10 +- .../AbstractAuthorizationTest.java | 7 + .../authorization/JSPolicyManagementTest.java | 7 + .../PolicyEnforcerClaimsTest.java | 7 + .../authorization/PolicyEnforcerTest.java | 2 + .../testsuite/authz/AbstractAuthzTest.java | 10 +- .../UserManagedPermissionServiceTest.java | 18 +- .../forms/ScriptAuthenticatorTest.java | 2 + .../oauth/OIDCProtocolMappersTest.java | 26 ++- .../DeployedScriptAuthenticatorTest.java | 217 ++++++++++++++++++ .../script/DeployedScriptMapperTest.java | 129 +++++++++++ .../script/DeployedScriptPolicyTest.java | 214 +++++++++++++++++ .../test/resources/scripts/mapper-example.js | 1 + .../AggregatePolicyManagementTest.java | 6 + .../authorization/JSPolicyManagementTest.java | 9 + .../admin/resources/js/authz/authz-app.js | 6 + .../resources/js/authz/authz-controller.js | 13 +- .../resource-server-policy-js-detail.html | 6 +- .../policy/resource-server-policy-list.html | 2 +- wildfly/server-subsystem/pom.xml | 10 + .../KeycloakProviderDeploymentProcessor.java | 3 + .../ScriptProviderDeploymentProcessor.java | 133 +++++++++++ 48 files changed, 1387 insertions(+), 74 deletions(-) create mode 100644 authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/DeployedScriptPolicyFactory.java create mode 100644 core/src/main/java/org/keycloak/representations/provider/ScriptProviderDescriptor.java create mode 100644 core/src/main/java/org/keycloak/representations/provider/ScriptProviderMetadata.java create mode 100644 services/src/main/java/org/keycloak/authentication/authenticators/browser/DeployedScriptAuthenticatorFactory.java create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/mappers/DeployedScriptOIDCProtocolMapper.java create mode 100644 services/src/main/java/org/keycloak/provider/DeploymentProviderLoader.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedScriptAuthenticatorTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedScriptMapperTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedScriptPolicyTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/scripts/mapper-example.js create mode 100644 wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ScriptProviderDeploymentProcessor.java diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/DeployedScriptPolicyFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/DeployedScriptPolicyFactory.java new file mode 100644 index 00000000000..4a039bdbbfc --- /dev/null +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/DeployedScriptPolicyFactory.java @@ -0,0 +1,84 @@ +/* + * Copyright 2019 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.authorization.policy.provider.js; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.Policy; +import org.keycloak.models.RealmModel; +import org.keycloak.models.ScriptModel; +import org.keycloak.representations.idm.authorization.JSPolicyRepresentation; +import org.keycloak.representations.provider.ScriptProviderMetadata; +import org.keycloak.scripting.ScriptingProvider; + +/** + * @author Pedro Igor + */ +public final class DeployedScriptPolicyFactory extends JSPolicyProviderFactory { + + private final ScriptProviderMetadata metadata; + + public DeployedScriptPolicyFactory(ScriptProviderMetadata metadata) { + this.metadata = metadata; + } + + @Override + public String getId() { + return metadata.getId(); + } + + @Override + public String getName() { + return metadata.getName(); + } + + @Override + protected boolean isDeployed() { + return true; + } + + @Override + public boolean isInternal() { + return false; + } + + @Override + public JSPolicyRepresentation toRepresentation(Policy policy, AuthorizationProvider authorization) { + JSPolicyRepresentation representation = new JSPolicyRepresentation(); + + representation.setId(policy.getId()); + representation.setName(policy.getName()); + representation.setDescription(metadata.getDescription()); + representation.setType(getId()); + representation.setCode(metadata.getCode()); + + return representation; + } + + @Override + protected ScriptModel getScriptModel(Policy policy, RealmModel realm, ScriptingProvider scripting) { + return scripting.createScript(realm.getId(), ScriptModel.TEXT_JAVASCRIPT, metadata.getName(), metadata.getCode(), + metadata.getDescription()); + } + + @Override + public void onCreate(Policy policy, JSPolicyRepresentation representation, AuthorizationProvider authorization) { + representation.setDescription(metadata.getDescription()); + policy.setDescription(metadata.getDescription()); + super.onCreate(policy, representation, authorization); + } +} diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java index 5de5db5a299..e0bee8da382 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java @@ -5,6 +5,7 @@ import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.policy.provider.PolicyProvider; import org.keycloak.authorization.policy.provider.PolicyProviderFactory; +import org.keycloak.common.Profile; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; @@ -56,17 +57,17 @@ public class JSPolicyProviderFactory implements PolicyProviderFactory { final ScriptingProvider scripting = authz.getKeycloakSession().getProvider(ScriptingProvider.class); @@ -104,7 +110,7 @@ public class JSPolicyProviderFactory implements PolicyProviderFactory> providers = new HashMap<>(); + + @JsonUnwrapped + @JsonGetter + public Map> getProviders() { + return providers; + } + + @JsonSetter + public void setAuthenticators(List metadata) { + providers.put(AUTHENTICATORS, metadata); + } + + @JsonSetter + public void setPolicies(List metadata) { + providers.put(POLICIES, metadata); + } + + @JsonSetter + public void setMappers(List metadata) { + providers.put(MAPPERS, metadata); + } + + public void addAuthenticator(String name, String fileName) { + addProvider(AUTHENTICATORS, name, fileName, null); + } + + private void addProvider(String type, String name, String fileName, String description) { + List authenticators = providers.get(type); + + if (authenticators == null) { + authenticators = new ArrayList<>(); + providers.put(type, authenticators); + } + + authenticators.add(new ScriptProviderMetadata(name, fileName, description)); + } + + public void addPolicy(String name, String fileName) { + addProvider(POLICIES, name, fileName, null); + } + + public void addMapper(String name, String fileName) { + addProvider(MAPPERS, name, fileName, null); + } +} diff --git a/core/src/main/java/org/keycloak/representations/provider/ScriptProviderMetadata.java b/core/src/main/java/org/keycloak/representations/provider/ScriptProviderMetadata.java new file mode 100644 index 00000000000..15512033203 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/provider/ScriptProviderMetadata.java @@ -0,0 +1,73 @@ +/* + * Copyright 2019 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.representations.provider; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public class ScriptProviderMetadata { + + @JsonIgnore + private String id; + private String name; + private String fileName; + private String description; + + @JsonIgnore + private String code; + + public ScriptProviderMetadata() { + + } + + public ScriptProviderMetadata(String name, String fileName, String description) { + this.name = name; + this.fileName = fileName; + this.description = description; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getFileName() { + return fileName; + } + + public String getDescription() { + return description; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } +} diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-wildfly-server-subsystem/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-wildfly-server-subsystem/main/module.xml index d038c7b1864..432bab5d94f 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-wildfly-server-subsystem/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-wildfly-server-subsystem/main/module.xml @@ -45,6 +45,10 @@ + + + + diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java b/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java index c92be9ec4af..3d455bd81ee 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java @@ -78,14 +78,12 @@ public final class AuthorizationProvider implements Provider { private final PolicyEvaluator policyEvaluator; private StoreFactory storeFactory; private StoreFactory storeFactoryDelegate; - private final Map policyProviderFactories; private final KeycloakSession keycloakSession; private final RealmModel realm; - public AuthorizationProvider(KeycloakSession session, RealmModel realm, Map policyProviderFactories, PolicyEvaluator policyEvaluator) { + public AuthorizationProvider(KeycloakSession session, RealmModel realm, PolicyEvaluator policyEvaluator) { this.keycloakSession = session; this.realm = realm; - this.policyProviderFactories = policyProviderFactories; this.policyEvaluator = policyEvaluator; } @@ -131,7 +129,8 @@ public final class AuthorizationProvider implements Provider { * @return a {@link List} containing all registered {@link PolicyProviderFactory} */ public Collection getProviderFactories() { - return this.policyProviderFactories.values(); + return keycloakSession.getKeycloakSessionFactory().getProviderFactories(PolicyProvider.class).stream().map( + PolicyProviderFactory.class::cast).collect(Collectors.toList()); } /** @@ -141,8 +140,8 @@ public final class AuthorizationProvider implements Provider { * @param the expected type of the provider * @return a {@link PolicyProviderFactory} with the given type */ - public F getProviderFactory(String type) { - return (F) policyProviderFactories.get(type); + public PolicyProviderFactory getProviderFactory(String type) { + return (PolicyProviderFactory) keycloakSession.getKeycloakSessionFactory().getProviderFactory(PolicyProvider.class, type); } /** @@ -153,7 +152,7 @@ public final class AuthorizationProvider implements Provider { * @return a {@link PolicyProvider} with the given type */ public

P getProvider(String type) { - PolicyProviderFactory policyProviderFactory = policyProviderFactories.get(type); + PolicyProviderFactory policyProviderFactory = getProviderFactory(type); if (policyProviderFactory == null) { return null; diff --git a/server-spi-private/src/main/java/org/keycloak/provider/KeycloakDeploymentInfo.java b/server-spi-private/src/main/java/org/keycloak/provider/KeycloakDeploymentInfo.java index 8b6c99a6e3f..23532774564 100644 --- a/server-spi-private/src/main/java/org/keycloak/provider/KeycloakDeploymentInfo.java +++ b/server-spi-private/src/main/java/org/keycloak/provider/KeycloakDeploymentInfo.java @@ -1,14 +1,20 @@ package org.keycloak.provider; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + public class KeycloakDeploymentInfo { private String name; private boolean services; private boolean themes; private boolean themeResources; + private Map, List> providers = new HashMap<>(); public boolean isProvider() { - return services || themes || themeResources; + return services || themes || themeResources || !providers.isEmpty(); } public boolean hasServices() { @@ -53,4 +59,12 @@ public class KeycloakDeploymentInfo { themeResources = true; return this; } + + public void addProvider(Class spi, ProviderFactory factory) { + providers.computeIfAbsent(spi, key -> new ArrayList<>()).add(factory); + } + + public Map, List> getProviders() { + return providers; + } } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/DeployedScriptAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/DeployedScriptAuthenticatorFactory.java new file mode 100644 index 00000000000..d8b4311a603 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/DeployedScriptAuthenticatorFactory.java @@ -0,0 +1,85 @@ +/* + * Copyright 2019 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.authentication.authenticators.browser; + +import java.util.HashMap; + +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.authentication.Authenticator; +import org.keycloak.common.Profile; +import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.representations.provider.ScriptProviderMetadata; + +/** + * @author Pedro Igor + */ +public final class DeployedScriptAuthenticatorFactory extends ScriptBasedAuthenticatorFactory { + + private final AuthenticatorConfigModel model; + + public DeployedScriptAuthenticatorFactory(ScriptProviderMetadata metadata) { + model = new AuthenticatorConfigModel(); + + model.setId(metadata.getId()); + model.setAlias(metadata.getName()); + model.setConfig(new HashMap<>()); + model.getConfig().put("scriptName", metadata.getName()); + model.getConfig().put("scriptCode", metadata.getCode()); + model.getConfig().put("scriptDescription", metadata.getDescription()); + } + + @Override + public Authenticator create(KeycloakSession session) { + return new ScriptBasedAuthenticator() { + @Override + protected AuthenticatorConfigModel getAuthenticatorConfig(AuthenticationFlowContext context) { + return model; + } + }; + } + + @Override + public String getId() { + return model.getId(); + } + + @Override + public boolean isConfigurable() { + return false; + } + + @Override + public boolean isUserSetupAllowed() { + return false; + } + + @Override + public String getDisplayType() { + return model.getAlias(); + } + + @Override + public String getHelpText() { + return model.getAlias(); + } + + @Override + public boolean isSupported() { + return Profile.isFeatureEnabled(Profile.Feature.SCRIPTS); + } +} diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java index c3d1de4f491..d7748209b82 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java @@ -20,6 +20,7 @@ import org.jboss.logging.Logger; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.Authenticator; +import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.ScriptModel; @@ -132,15 +133,21 @@ public class ScriptBasedAuthenticator implements Authenticator { } private boolean hasAuthenticatorConfig(AuthenticationFlowContext context) { - return context != null - && context.getAuthenticatorConfig() != null - && context.getAuthenticatorConfig().getConfig() != null - && !context.getAuthenticatorConfig().getConfig().isEmpty(); + if (context == null) + return false; + AuthenticatorConfigModel config = getAuthenticatorConfig(context); + return config != null + && config.getConfig() != null + && !config.getConfig().isEmpty(); + } + + protected AuthenticatorConfigModel getAuthenticatorConfig(AuthenticationFlowContext context) { + return context.getAuthenticatorConfig(); } private InvocableScriptAdapter getInvocableScriptAdapter(AuthenticationFlowContext context) { - Map config = context.getAuthenticatorConfig().getConfig(); + Map config = getAuthenticatorConfig(context).getConfig(); String scriptName = config.get(SCRIPT_NAME); String scriptCode = config.get(SCRIPT_CODE); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java index f5fabe25bbd..82656a60cd9 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java @@ -153,6 +153,6 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory, En @Override public boolean isSupported() { - return Profile.isFeatureEnabled(Profile.Feature.SCRIPTS); + return Profile.isFeatureEnabled(Profile.Feature.SCRIPTS) && Profile.isFeatureEnabled(Profile.Feature.UPLOAD_SCRIPTS); } } diff --git a/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java b/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java index 24390b3666a..a45ff5db5af 100644 --- a/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java +++ b/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java @@ -18,26 +18,18 @@ package org.keycloak.authorization; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - import org.keycloak.Config; import org.keycloak.authorization.policy.evaluation.DefaultPolicyEvaluator; import org.keycloak.authorization.policy.evaluation.PolicyEvaluator; -import org.keycloak.authorization.policy.provider.PolicyProvider; -import org.keycloak.authorization.policy.provider.PolicyProviderFactory; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; -import org.keycloak.provider.ProviderFactory; /** * @author Pedro Igor */ public class DefaultAuthorizationProviderFactory implements AuthorizationProviderFactory { - private Map policyProviderFactories; private PolicyEvaluator policyEvaluator = new DefaultPolicyEvaluator(); @Override @@ -51,7 +43,6 @@ public class DefaultAuthorizationProviderFactory implements AuthorizationProvide @Override public void postInit(KeycloakSessionFactory factory) { - policyProviderFactories = configurePolicyProviderFactories(factory); } @Override @@ -66,21 +57,6 @@ public class DefaultAuthorizationProviderFactory implements AuthorizationProvide @Override public AuthorizationProvider create(KeycloakSession session, RealmModel realm) { - return new AuthorizationProvider(session, realm, policyProviderFactories, policyEvaluator); + return new AuthorizationProvider(session, realm, policyEvaluator); } - - private Map configurePolicyProviderFactories(KeycloakSessionFactory keycloakSessionFactory) { - List providerFactories = keycloakSessionFactory.getProviderFactories(PolicyProvider.class); - - if (providerFactories.isEmpty()) { - throw new RuntimeException("Could not find any policy provider."); - } - - HashMap providers = new HashMap<>(); - - providerFactories.forEach(providerFactory -> providers.put(providerFactory.getId(), (PolicyProviderFactory) providerFactory)); - - return providers; - } - -} \ No newline at end of file +} diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyResourceService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyResourceService.java index 361b4661265..6ee162cbd60 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyResourceService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyResourceService.java @@ -109,7 +109,9 @@ public class PolicyResourceService { PolicyStore policyStore = storeFactory.getPolicyStore(); PolicyProviderFactory resource = getProviderFactory(policy.getType()); - resource.onRemove(policy, authorization); + if (resource != null) { + resource.onRemove(policy, authorization); + } policyStore.delete(policy.getId()); diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java index 7b350c649a4..c7f6d36551f 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java @@ -205,6 +205,8 @@ public class ResourceServerService { defaultPolicyConfig.put("code", "// by default, grants any permission associated with this policy\n$evaluation.grant();\n"); defaultPolicy.setConfig(defaultPolicyConfig); + + session.setAttribute("ALLOW_CREATE_POLICY", true); getPolicyResource().create(defaultPolicy); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/DeployedScriptOIDCProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/DeployedScriptOIDCProtocolMapper.java new file mode 100644 index 00000000000..122065a1c43 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/DeployedScriptOIDCProtocolMapper.java @@ -0,0 +1,80 @@ +/* + * Copyright 2019 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.protocol.oidc.mappers; + +import java.util.List; + +import org.keycloak.common.Profile; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.protocol.ProtocolMapperUtils; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; +import org.keycloak.representations.provider.ScriptProviderMetadata; + +public final class DeployedScriptOIDCProtocolMapper extends ScriptBasedOIDCProtocolMapper { + + private static final List configProperties; + + static { + configProperties = ProviderConfigurationBuilder.create() + .property() + .name(ProtocolMapperUtils.MULTIVALUED) + .label(ProtocolMapperUtils.MULTIVALUED_LABEL) + .helpText(ProtocolMapperUtils.MULTIVALUED_HELP_TEXT) + .type(ProviderConfigProperty.BOOLEAN_TYPE) + .defaultValue(false) + .add() + .build(); + + OIDCAttributeMapperHelper.addAttributeConfig(configProperties, UserPropertyMapper.class); + } + + private final ScriptProviderMetadata metadata; + + public DeployedScriptOIDCProtocolMapper(ScriptProviderMetadata metadata) { + this.metadata = metadata; + } + + @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 getConfigProperties() { + return configProperties; + } + + @Override + public boolean isSupported() { + return Profile.isFeatureEnabled(Profile.Feature.SCRIPTS); + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/ScriptBasedOIDCProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/ScriptBasedOIDCProtocolMapper.java index 6e75d32315b..35dfec4c572 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/ScriptBasedOIDCProtocolMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/ScriptBasedOIDCProtocolMapper.java @@ -29,6 +29,7 @@ import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.ProtocolMapperConfigException; import org.keycloak.protocol.ProtocolMapperUtils; +import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.representations.IDToken; @@ -43,7 +44,8 @@ import java.util.List; * * @author Thomas Darimont */ -public class ScriptBasedOIDCProtocolMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper { +public class ScriptBasedOIDCProtocolMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper, + EnvironmentDependentProviderFactory { public static final String PROVIDER_ID = "oidc-script-based-protocol-mapper"; @@ -115,8 +117,9 @@ public class ScriptBasedOIDCProtocolMapper extends AbstractOIDCProtocolMapper im return "Evaluates a JavaScript function to produce a token claim based on context information."; } + @Override public boolean isSupported() { - return Profile.isFeatureEnabled(Profile.Feature.SCRIPTS); + return Profile.isFeatureEnabled(Profile.Feature.SCRIPTS) && Profile.isFeatureEnabled(Profile.Feature.UPLOAD_SCRIPTS); } @Override @@ -128,7 +131,7 @@ public class ScriptBasedOIDCProtocolMapper extends AbstractOIDCProtocolMapper im protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) { UserModel user = userSession.getUser(); - String scriptSource = mappingModel.getConfig().get(SCRIPT); + String scriptSource = getScriptCode(mappingModel); RealmModel realm = userSession.getRealm(); ScriptingProvider scripting = keycloakSession.getProvider(ScriptingProvider.class); @@ -156,7 +159,7 @@ public class ScriptBasedOIDCProtocolMapper extends AbstractOIDCProtocolMapper im @Override public void validateConfig(KeycloakSession session, RealmModel realm, ProtocolMapperContainerModel client, ProtocolMapperModel mapperModel) throws ProtocolMapperConfigException { - String scriptCode = mapperModel.getConfig().get(SCRIPT); + String scriptCode = getScriptCode(mapperModel); if (scriptCode == null) { return; } @@ -171,6 +174,10 @@ public class ScriptBasedOIDCProtocolMapper extends AbstractOIDCProtocolMapper im } } + protected String getScriptCode(ProtocolMapperModel mapperModel) { + return mapperModel.getConfig().get(SCRIPT); + } + public static ProtocolMapperModel create(String name, String userAttribute, String tokenClaimName, String claimType, diff --git a/services/src/main/java/org/keycloak/provider/DeploymentProviderLoader.java b/services/src/main/java/org/keycloak/provider/DeploymentProviderLoader.java new file mode 100644 index 00000000000..e7af85c4b35 --- /dev/null +++ b/services/src/main/java/org/keycloak/provider/DeploymentProviderLoader.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.provider; + +import java.util.Collections; +import java.util.List; + +/** + * @author Pedro Igor + */ +final class DeploymentProviderLoader implements ProviderLoader { + + private final KeycloakDeploymentInfo info; + + DeploymentProviderLoader(KeycloakDeploymentInfo info) { + this.info = info; + } + + @Override + public List loadSpis() { + return Collections.emptyList(); + } + + @Override + public List load(Spi spi) { + return info.getProviders().getOrDefault(spi.getClass(), Collections.emptyList()); + } +} diff --git a/services/src/main/java/org/keycloak/provider/ProviderManager.java b/services/src/main/java/org/keycloak/provider/ProviderManager.java index e7659a4c2c1..9355a7c343a 100644 --- a/services/src/main/java/org/keycloak/provider/ProviderManager.java +++ b/services/src/main/java/org/keycloak/provider/ProviderManager.java @@ -49,6 +49,7 @@ public class ProviderManager { logger.debugv("Provider loaders {0}", factories); loaders.add(new DefaultProviderLoader(info, baseClassLoader)); + loaders.add(new DeploymentProviderLoader(info)); if (resources != null) { for (String r : resources) { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java index 710746739f8..174b2718fa5 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java @@ -77,6 +77,7 @@ public class ProfileAssume { } private static boolean isFeatureEnabled(Profile.Feature feature) { + updateProfile(); return !disabledFeatures.contains(feature.name()); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java index e0cb0d81f3f..b058ff4be1c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java @@ -32,6 +32,7 @@ import org.keycloak.admin.client.resource.AuthenticationManagementResource; import org.keycloak.admin.client.resource.RealmsResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.common.Profile; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.Time; import org.keycloak.representations.idm.ClientRepresentation; @@ -61,6 +62,7 @@ import org.keycloak.testsuite.util.TestEventsLogger; import org.openqa.selenium.WebDriver; import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import java.io.IOException; @@ -81,6 +83,7 @@ import java.util.stream.Collectors; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.keycloak.testsuite.admin.Users.setPasswordFor; import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN; @@ -149,6 +152,7 @@ public abstract class AbstractKeycloakTest { private PropertiesConfiguration constantsProperties; private boolean resetTimeOffset; + private List enabledFeatures = new ArrayList<>(); @Before public void beforeAbstractKeycloakTest() throws Exception { @@ -226,6 +230,10 @@ public abstract class AbstractKeycloakTest { testContext.getCleanups().clear(); } + for (Profile.Feature feature : enabledFeatures) { + disableFeature(feature); + } + postAfterAbstractKeycloak(); // Remove all browsers from queue @@ -631,4 +639,17 @@ public abstract class AbstractKeycloakTest { } return in; } + + protected void enableFeature(Profile.Feature feature) { + enabledFeatures.add(feature); + try (Response response = getTestingClient().testing().enableFeature(feature.toString())) { + assertEquals(200, response.getStatus()); + } + } + + protected void disableFeature(Profile.Feature feature) { + try (Response response = getTestingClient().testing().disableFeature(feature.toString())) { + assertEquals(200, response.getStatus()); + } + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBasePhotozExampleAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBasePhotozExampleAdapterTest.java index 9973b3152ba..741b07602d1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBasePhotozExampleAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBasePhotozExampleAdapterTest.java @@ -21,6 +21,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.keycloak.common.Profile.Feature.UPLOAD_SCRIPTS; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad; import static org.keycloak.testsuite.utils.io.IOUtil.loadJson; @@ -164,6 +165,7 @@ public abstract class AbstractBasePhotozExampleAdapterTest extends AbstractPhoto @Override public void addAdapterTestRealms(List testRealms) { + enableFeature(UPLOAD_SCRIPTS); RealmRepresentation realm = loadRealm(new File(TEST_APPS_HOME_DIR + "/photoz/photoz-realm.json")); realm.setAccessTokenLifespan(30 + TOKEN_LIFESPAN_LEEWAY); // seconds diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBaseServletAuthzAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBaseServletAuthzAdapterTest.java index 0753f982ae8..5b0cce7c7b7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBaseServletAuthzAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractBaseServletAuthzAdapterTest.java @@ -18,7 +18,7 @@ package org.keycloak.testsuite.adapter.example.authorization; import org.jboss.arquillian.container.test.api.Deployer; import org.jboss.arquillian.test.api.ArquillianResource; -import org.junit.BeforeClass; +import org.junit.Before; import org.keycloak.admin.client.resource.AuthorizationResource; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientsResource; @@ -27,7 +27,6 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.authorization.PolicyRepresentation; import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; import org.keycloak.representations.idm.authorization.UserPolicyRepresentation; -import org.keycloak.testsuite.ProfileAssume; import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest; import org.keycloak.testsuite.util.UIUtils; import org.openqa.selenium.By; @@ -42,6 +41,7 @@ import java.net.URL; import java.util.List; import static org.junit.Assert.assertFalse; +import static org.keycloak.common.Profile.Feature.UPLOAD_SCRIPTS; import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad; import static org.keycloak.testsuite.utils.io.IOUtil.loadJson; import static org.keycloak.testsuite.utils.io.IOUtil.loadRealm; @@ -58,6 +58,11 @@ public abstract class AbstractBaseServletAuthzAdapterTest extends AbstractExampl @ArquillianResource private Deployer deployer; + @Before + public void onBefore() { + enableFeature(UPLOAD_SCRIPTS); + } + @Override public void addAdapterTestRealms(List testRealms) { testRealms.add( diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/ServletPolicyEnforcerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/ServletPolicyEnforcerTest.java index 917be6aca69..0bf4cbe3301 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/ServletPolicyEnforcerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/ServletPolicyEnforcerTest.java @@ -18,6 +18,7 @@ package org.keycloak.testsuite.adapter.example.authorization; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.keycloak.common.Profile.Feature.UPLOAD_SCRIPTS; import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad; import static org.keycloak.testsuite.utils.io.IOUtil.loadRealm; @@ -32,6 +33,7 @@ import org.jboss.arquillian.container.test.api.Deployer; import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.test.api.ArquillianResource; import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.BeforeClass; import org.junit.Test; import org.keycloak.admin.client.resource.AuthorizationResource; import org.keycloak.admin.client.resource.ClientResource; @@ -41,6 +43,7 @@ import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.testsuite.ProfileAssume; import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest; import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; import org.keycloak.testsuite.utils.arquillian.ContainerConstants; @@ -69,6 +72,7 @@ public class ServletPolicyEnforcerTest extends AbstractExampleAdapterTest { @Override public void addAdapterTestRealms(List testRealms) { + enableFeature(UPLOAD_SCRIPTS); testRealms.add( loadRealm(new File(TEST_APPS_HOME_DIR + "/servlet-policy-enforcer/servlet-policy-enforcer-authz-realm.json"))); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/cors/CorsExampleAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/cors/CorsExampleAdapterTest.java index 7f98d3f579d..1500157432b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/cors/CorsExampleAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/cors/CorsExampleAdapterTest.java @@ -28,6 +28,7 @@ import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.Test; +import org.keycloak.common.Profile; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest; import org.keycloak.testsuite.adapter.page.AngularCorsProductTestApp; @@ -108,6 +109,7 @@ public class CorsExampleAdapterTest extends AbstractExampleAdapterTest { @Override public void addAdapterTestRealms(List testRealms) { + enableFeature(Profile.Feature.UPLOAD_SCRIPTS); testRealms.add( loadRealm(new File(TEST_APPS_HOME_DIR + "/cors/cors-realm.json"))); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java index 5aa4387dac8..1230364ae19 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java @@ -50,6 +50,7 @@ import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentati import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.authorization.PolicyRepresentation; +import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; import org.keycloak.representations.idm.authorization.ScopeRepresentation; @@ -957,13 +958,10 @@ public class PermissionsTest extends AbstractKeycloakTest { invoke(new InvocationWithResponse() { public void invoke(RealmResource realm, AtomicReference response) { AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); - PolicyRepresentation representation = new PolicyRepresentation(); + ResourcePermissionRepresentation representation = new ResourcePermissionRepresentation(); representation.setName("Test PermissionsTest"); - representation.setType("js"); - HashMap config = new HashMap<>(); - config.put("code", ""); - representation.setConfig(config); - response.set(authorization.policies().create(representation)); + representation.addResource("Default Resource"); + response.set(authorization.permissions().resource().create(representation)); } }, AUTHORIZATION, true); invoke(new Invocation() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java index 8520e7d2a85..ffe3764c529 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java @@ -18,6 +18,7 @@ package org.keycloak.testsuite.admin.client.authorization; import org.junit.After; +import org.junit.Before; import org.junit.BeforeClass; import org.keycloak.admin.client.resource.AuthorizationResource; import org.keycloak.admin.client.resource.ClientResource; @@ -36,6 +37,7 @@ import org.keycloak.testsuite.util.UserBuilder; import javax.ws.rs.core.Response; import static org.junit.Assert.assertEquals; +import static org.keycloak.common.Profile.Feature.UPLOAD_SCRIPTS; import java.util.List; @@ -46,6 +48,11 @@ public abstract class AbstractAuthorizationTest extends AbstractClientTest { protected static final String RESOURCE_SERVER_CLIENT_ID = "resource-server-test"; + @Before + public void onBefore() { + enableFeature(UPLOAD_SCRIPTS); + } + @Override public void setDefaultPageUriParameters() { super.setDefaultPageUriParameters(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/JSPolicyManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/JSPolicyManagementTest.java index bec418d8ee5..a0d4dd31c24 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/JSPolicyManagementTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/JSPolicyManagementTest.java @@ -24,10 +24,12 @@ import java.util.Collections; import javax.ws.rs.NotFoundException; import javax.ws.rs.core.Response; +import org.junit.Before; import org.junit.Test; import org.keycloak.admin.client.resource.AuthorizationResource; import org.keycloak.admin.client.resource.JSPoliciesResource; import org.keycloak.admin.client.resource.JSPolicyResource; +import org.keycloak.common.Profile; import org.keycloak.representations.idm.authorization.DecisionStrategy; import org.keycloak.representations.idm.authorization.JSPolicyRepresentation; import org.keycloak.representations.idm.authorization.Logic; @@ -37,6 +39,11 @@ import org.keycloak.representations.idm.authorization.Logic; */ public class JSPolicyManagementTest extends AbstractPolicyManagementTest { + @Before + public void onBefore() { + enableFeature(Profile.Feature.UPLOAD_SCRIPTS); + } + @Test public void testCreate() { AuthorizationResource authorization = getClient().authorization(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerClaimsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerClaimsTest.java index c89a06fb509..e35b3d7cc26 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerClaimsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerClaimsTest.java @@ -35,6 +35,7 @@ import java.util.stream.Collectors; import javax.security.cert.X509Certificate; +import org.junit.Before; import org.junit.Test; import org.keycloak.AuthorizationContext; import org.keycloak.KeycloakSecurityContext; @@ -51,6 +52,7 @@ import org.keycloak.adapters.spi.LogoutError; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.authorization.client.AuthzClient; +import org.keycloak.common.Profile; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInputException; import org.keycloak.representations.AccessToken; @@ -107,6 +109,11 @@ public class PolicyEnforcerClaimsTest extends AbstractKeycloakTest { .directAccessGrants()) .build()); } + + @Before + public void onBefore() { + enableFeature(Profile.Feature.UPLOAD_SCRIPTS); + } @Test public void testEnforceUMAAccessWithClaimsUsingBearerToken() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerTest.java index fa00ba7d3cc..084164335eb 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/PolicyEnforcerTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.keycloak.common.Profile.Feature.UPLOAD_SCRIPTS; import javax.security.cert.X509Certificate; import javax.ws.rs.HttpMethod; @@ -126,6 +127,7 @@ public class PolicyEnforcerTest extends AbstractKeycloakTest { @Before public void onBefore() { + enableFeature(UPLOAD_SCRIPTS); initAuthorizationSettings(getClientResource(RESOURCE_SERVER_CLIENT_ID)); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractAuthzTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractAuthzTest.java index 6a5680b6fd8..10f1b01e36d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractAuthzTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/AbstractAuthzTest.java @@ -1,17 +1,23 @@ package org.keycloak.testsuite.authz; -import org.junit.BeforeClass; +import static org.keycloak.common.Profile.Feature.UPLOAD_SCRIPTS; + +import org.junit.Before; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInputException; import org.keycloak.representations.AccessToken; import org.keycloak.testsuite.AbstractKeycloakTest; -import org.keycloak.testsuite.ProfileAssume; /** * @author mhajas */ public abstract class AbstractAuthzTest extends AbstractKeycloakTest { + @Before + public void onBefore() { + enableFeature(UPLOAD_SCRIPTS); + } + protected AccessToken toAccessToken(String rpt) { AccessToken accessToken; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedPermissionServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedPermissionServiceTest.java index b0c445cba53..d37393592e1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedPermissionServiceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/UserManagedPermissionServiceTest.java @@ -96,8 +96,7 @@ public class UserManagedPermissionServiceTest extends AbstractResourceServerTest .build()); } - @Test - public void testCreate() { + private void testCreate() { ResourceRepresentation resource = new ResourceRepresentation(); resource.setName("Resource A"); @@ -148,7 +147,12 @@ public class UserManagedPermissionServiceTest extends AbstractResourceServerTest } @Test - public void testUpdate() { + public void testCreateDeprecatedFeaturesDisabled() { + ProfileAssume.assumeFeatureDisabled(Profile.Feature.UPLOAD_SCRIPTS); + testCreate(); + } + + private void testUpdate() { ResourceRepresentation resource = new ResourceRepresentation(); resource.setName("Resource A"); @@ -336,10 +340,16 @@ public class UserManagedPermissionServiceTest extends AbstractResourceServerTest ProfileAssume.assumeFeatureEnabled(Profile.Feature.UPLOAD_SCRIPTS); testUpdate(); } + + @Test + public void testUpdateDeprecatedFeaturesDisabled() { + ProfileAssume.assumeFeatureDisabled(Profile.Feature.UPLOAD_SCRIPTS); + testUpdate(); + } @Test public void testUploadScriptDisabled() { - ProfileAssume.assumeFeatureDisabled(Profile.Feature.UPLOAD_SCRIPTS); + disableFeature(Profile.Feature.UPLOAD_SCRIPTS); ResourceRepresentation resource = new ResourceRepresentation(); resource.setName("Resource A"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ScriptAuthenticatorTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ScriptAuthenticatorTest.java index ec042b293c7..fb408ea0375 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ScriptAuthenticatorTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ScriptAuthenticatorTest.java @@ -67,7 +67,9 @@ public class ScriptAuthenticatorTest extends AbstractFlowTest { @BeforeClass public static void verifyEnvironment() { + // TODO: we should probably enable SCRIPTS automatically when UPLOAD_SCRIPTS is enabled ProfileAssume.assumeFeatureEnabled(Profile.Feature.SCRIPTS); + ProfileAssume.assumeFeatureEnabled(Profile.Feature.UPLOAD_SCRIPTS); } @Override diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java index e21b47a29eb..86a65f63805 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OIDCProtocolMappersTest.java @@ -26,6 +26,7 @@ import org.keycloak.admin.client.resource.ClientScopeResource; import org.keycloak.admin.client.resource.ProtocolMappersResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.common.Profile; import org.keycloak.common.util.UriUtils; import org.keycloak.jose.jws.JWSInput; import org.keycloak.models.AccountRoles; @@ -45,6 +46,7 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.ProfileAssume; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; @@ -126,6 +128,27 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest { testRealms.add(realm); } + @Test + public void testTokenScriptMapping() { + ProfileAssume.assumeFeatureEnabled(Profile.Feature.UPLOAD_SCRIPTS); + { + ClientResource app = findClientResourceByClientId(adminClient.realm("test"), "test-app"); + + app.getProtocolMappers().createMapper(createScriptMapper("test-script-mapper1","computed-via-script", "computed-via-script", "String", true, true, "'hello_' + user.username", false)).close(); + app.getProtocolMappers().createMapper(createScriptMapper("test-script-mapper2","multiValued-via-script", "multiValued-via-script", "String", true, true, "new java.util.ArrayList(['A','B'])", true)).close(); + + Response response = app.getProtocolMappers().createMapper(createScriptMapper("test-script-mapper3", "syntax-error-script", "syntax-error-script", "String", true, true, "func_tion foo(){ return 'fail';} foo()", false)); + assertThat(response.getStatusInfo().getFamily(), is(Response.Status.Family.CLIENT_ERROR)); + response.close(); + } + { + OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password"); + AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); + + assertEquals("hello_test-user@localhost", accessToken.getOtherClaims().get("computed-via-script")); + assertEquals(Arrays.asList("A","B"), accessToken.getOtherClaims().get("multiValued-via-script")); + } + } @Test public void testTokenMapping() throws Exception { @@ -242,9 +265,6 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest { Assert.assertNull(accessToken.getResourceAccess("test-app")); assertTrue(accessToken.getResourceAccess("app").getRoles().contains("hardcoded")); - assertEquals("hello_test-user@localhost", accessToken.getOtherClaims().get("computed-via-script")); - assertEquals(Arrays.asList("A","B"), accessToken.getOtherClaims().get("multiValued-via-script")); - // Assert audiences added through AudienceResolve mapper Assert.assertThat(accessToken.getAudience(), arrayContainingInAnyOrder( "app", "account")); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedScriptAuthenticatorTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedScriptAuthenticatorTest.java new file mode 100644 index 00000000000..16ce76b1227 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedScriptAuthenticatorTest.java @@ -0,0 +1,217 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.script; + +import static org.junit.Assert.assertFalse; +import static org.keycloak.common.Profile.Feature.SCRIPTS; +import static org.keycloak.common.Profile.Feature.UPLOAD_SCRIPTS; +import static org.keycloak.testsuite.arquillian.DeploymentTargetModifier.AUTH_SERVER_CURRENT; + +import java.io.IOException; + +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.graphene.page.Page; +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.Rule; +import org.junit.Test; +import org.keycloak.authentication.authenticators.browser.ScriptBasedAuthenticatorFactory; +import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventType; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; +import org.keycloak.representations.idm.AuthenticationFlowRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.representations.provider.ScriptProviderDescriptor; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.ProfileAssume; +import org.keycloak.testsuite.forms.AbstractFlowTest; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.util.ContainerAssume; +import org.keycloak.testsuite.util.ExecutionBuilder; +import org.keycloak.testsuite.util.FlowBuilder; +import org.keycloak.testsuite.util.RealmBuilder; +import org.keycloak.testsuite.util.UserBuilder; +import org.keycloak.util.JsonSerialization; + +/** + * @author Pedro Igor + */ +public class DeployedScriptAuthenticatorTest extends AbstractFlowTest { + + public static final String EXECUTION_ID = "scriptAuth"; + private static final String SCRIPT_DEPLOYMENT_NAME = "scripts.jar"; + + @Deployment(name = SCRIPT_DEPLOYMENT_NAME, managed = false, testable = false) + @TargetsContainer(AUTH_SERVER_CURRENT) + public static JavaArchive deploy() throws IOException { + ScriptProviderDescriptor representation = new ScriptProviderDescriptor(); + + representation.addAuthenticator("My Authenticator", "authenticator-a.js"); + + return ShrinkWrap.create(JavaArchive.class, SCRIPT_DEPLOYMENT_NAME) + .addAsManifestResource(new StringAsset(JsonSerialization.writeValueAsPrettyString(representation)), + "keycloak-scripts.json") + .addAsResource("scripts/authenticator-example.js", "authenticator-a.js"); + } + + @BeforeClass + public static void verifyEnvironment() { + ContainerAssume.assumeNotAuthServerUndertow(); + } + + @Rule + public AssertEvents events = new AssertEvents(this); + + @Page + protected LoginPage loginPage; + + @ArquillianResource + private Deployer deployer; + + private AuthenticationFlowRepresentation flow; + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + UserRepresentation failUser = UserBuilder.create() + .id("fail") + .username("fail") + .email("fail@test.com") + .enabled(true) + .password("password") + .build(); + + UserRepresentation okayUser = UserBuilder.create() + .id("user") + .username("user") + .email("user@test.com") + .enabled(true) + .password("password") + .build(); + + RealmBuilder.edit(testRealm) + .user(failUser) + .user(okayUser); + } + + public void configureFlows() { + deployer.deploy(SCRIPT_DEPLOYMENT_NAME); + + if (testContext.isInitialized()) { + return; + } + + String scriptFlow = "scriptBrowser"; + + AuthenticationFlowRepresentation scriptBrowserFlow = FlowBuilder.create() + .alias(scriptFlow) + .description("dummy pass through registration") + .providerId("basic-flow") + .topLevel(true) + .builtIn(false) + .build(); + + Response createFlowResponse = testRealm().flows().createFlow(scriptBrowserFlow); + Assert.assertEquals(201, createFlowResponse.getStatus()); + + RealmRepresentation realm = testRealm().toRepresentation(); + realm.setBrowserFlow(scriptFlow); + realm.setDirectGrantFlow(scriptFlow); + testRealm().update(realm); + + this.flow = findFlowByAlias(scriptFlow); + + AuthenticationExecutionRepresentation usernamePasswordFormExecution = ExecutionBuilder.create() + .id("username password form") + .parentFlow(this.flow.getId()) + .requirement(AuthenticationExecutionModel.Requirement.REQUIRED.name()) + .authenticator(UsernamePasswordFormFactory.PROVIDER_ID) + .build(); + + AuthenticationExecutionRepresentation authScriptExecution = ExecutionBuilder.create() + .id(EXECUTION_ID) + .parentFlow(this.flow.getId()) + .requirement(AuthenticationExecutionModel.Requirement.REQUIRED.name()) + .authenticator("script-authenticator-a.js") + .build(); + + Response addExecutionResponse = testRealm().flows().addExecution(usernamePasswordFormExecution); + Assert.assertEquals(201, addExecutionResponse.getStatus()); + addExecutionResponse.close(); + + addExecutionResponse = testRealm().flows().addExecution(authScriptExecution); + Assert.assertEquals(201, addExecutionResponse.getStatus()); + addExecutionResponse.close(); + + testContext.setInitialized(true); + } + + @After + public void onAfter() { + deployer.undeploy(SCRIPT_DEPLOYMENT_NAME); + } + + /** + * KEYCLOAK-3491 + */ + @Test + public void loginShouldWorkWithScriptAuthenticator() { + ProfileAssume.assumeFeatureEnabled(SCRIPTS); + configureFlows(); + + loginPage.open(); + + loginPage.login("user", "password"); + + events.expectLogin().user("user").detail(Details.USERNAME, "user").assertEvent(); + } + + /** + * KEYCLOAK-3491 + */ + @Test + public void loginShouldFailWithScriptAuthenticator() { + ProfileAssume.assumeFeatureEnabled(SCRIPTS); + configureFlows(); + + loginPage.open(); + + loginPage.login("fail", "password"); + + events.expect(EventType.LOGIN_ERROR).user((String) null).error(Errors.USER_NOT_FOUND).assertEvent(); + } + + @Test + public void testScriptAuthenticatorNotAvailable() { + ProfileAssume.assumeFeatureDisabled(UPLOAD_SCRIPTS); + assertFalse(testRealm().flows().getAuthenticatorProviders().stream().anyMatch( + provider -> ScriptBasedAuthenticatorFactory.PROVIDER_ID.equals(provider.get("id")))); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedScriptMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedScriptMapperTest.java new file mode 100644 index 00000000000..966c2053b99 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedScriptMapperTest.java @@ -0,0 +1,129 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.script; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.keycloak.common.Profile.Feature.UPLOAD_SCRIPTS; +import static org.keycloak.testsuite.admin.ApiUtil.findClientResourceByClientId; +import static org.keycloak.testsuite.arquillian.DeploymentTargetModifier.AUTH_SERVER_CURRENT; +import static org.keycloak.testsuite.util.ProtocolMapperUtil.createScriptMapper; + +import java.io.IOException; + +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.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.common.Profile; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.mappers.ScriptBasedOIDCProtocolMapper; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.provider.ScriptProviderDescriptor; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.ProfileAssume; +import org.keycloak.testsuite.util.ContainerAssume; +import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.util.JsonSerialization; + +/** + * @author Pedro Igor + */ +public class DeployedScriptMapperTest extends AbstractTestRealmKeycloakTest { + + private static final String SCRIPT_DEPLOYMENT_NAME = "scripts.jar"; + + @Deployment(name = SCRIPT_DEPLOYMENT_NAME, managed = false, testable = false) + @TargetsContainer(AUTH_SERVER_CURRENT) + public static JavaArchive deploy() throws IOException { + ScriptProviderDescriptor representation = new ScriptProviderDescriptor(); + + representation.addMapper("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 configureFlows() { + deployer.deploy(SCRIPT_DEPLOYMENT_NAME); + } + + @After + public void onAfter() { + deployer.undeploy(SCRIPT_DEPLOYMENT_NAME); + } + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + + } + + @Test + public void testScriptMapperNotAvailable() { + ProfileAssume.assumeFeatureDisabled(UPLOAD_SCRIPTS); + assertFalse(adminClient.serverInfo().getInfo().getProtocolMapperTypes().get(OIDCLoginProtocol.LOGIN_PROTOCOL).stream() + .anyMatch( + mapper -> ScriptBasedOIDCProtocolMapper.PROVIDER_ID.equals(mapper.getId()))); + } + + @Test + public void testTokenScriptMapping() { + ProfileAssume.assumeFeatureEnabled(Profile.Feature.SCRIPTS); + { + ClientResource app = findClientResourceByClientId(adminClient.realm("test"), "test-app"); + + ProtocolMapperRepresentation mapper = createScriptMapper("test-script-mapper1", "computed-via-script", + "computed-via-script", "String", true, true, "'hello_' + user.username", false); + + mapper.setProtocolMapper("script-mapper-a.js"); + + app.getProtocolMappers().createMapper(mapper).close(); + } + { + OAuthClient.AccessTokenResponse response = browserLogin("password", "test-user@localhost", "password"); + AccessToken accessToken = oauth.verifyToken(response.getAccessToken()); + + assertEquals("hello_test-user@localhost", accessToken.getOtherClaims().get("computed-via-script")); + } + } + + private OAuthClient.AccessTokenResponse browserLogin(String clientSecret, String username, String password) { + OAuthClient.AuthorizationEndpointResponse authzEndpointResponse = oauth.doLogin(username, password); + return oauth.doAccessTokenRequest(authzEndpointResponse.getCode(), clientSecret); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedScriptPolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedScriptPolicyTest.java new file mode 100644 index 00000000000..45b358154ca --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/script/DeployedScriptPolicyTest.java @@ -0,0 +1,214 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.script; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.keycloak.common.Profile.Feature.UPLOAD_SCRIPTS; +import static org.keycloak.testsuite.arquillian.DeploymentTargetModifier.AUTH_SERVER_CURRENT; + +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.List; + +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.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.keycloak.admin.client.resource.AuthorizationResource; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ClientsResource; +import org.keycloak.admin.client.resource.PermissionsResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.authorization.DecisionEffect; +import org.keycloak.representations.idm.authorization.DecisionStrategy; +import org.keycloak.representations.idm.authorization.JSPolicyRepresentation; +import org.keycloak.representations.idm.authorization.PolicyEvaluationRequest; +import org.keycloak.representations.idm.authorization.PolicyEvaluationResponse; +import org.keycloak.representations.idm.authorization.PolicyRepresentation; +import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.representations.provider.ScriptProviderDescriptor; +import org.keycloak.testsuite.ProfileAssume; +import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected; +import org.keycloak.testsuite.authz.AbstractAuthzTest; +import org.keycloak.testsuite.util.ClientBuilder; +import org.keycloak.testsuite.util.ContainerAssume; +import org.keycloak.testsuite.util.RealmBuilder; +import org.keycloak.testsuite.util.RoleBuilder; +import org.keycloak.testsuite.util.RolesBuilder; +import org.keycloak.testsuite.util.UserBuilder; +import org.keycloak.util.JsonSerialization; + +/** + * @author Pedro Igor + */ +public class DeployedScriptPolicyTest extends AbstractAuthzTest { + + private static final String SCRIPT_DEPLOYMENT_NAME = "scripts.jar"; + + @Deployment(name = SCRIPT_DEPLOYMENT_NAME, managed = false, testable = false) + @TargetsContainer(AUTH_SERVER_CURRENT) + public static JavaArchive deploy() throws IOException { + ScriptProviderDescriptor representation = new ScriptProviderDescriptor(); + + representation.addPolicy("Grant Policy", "policy-grant.js"); + representation.addPolicy("Deny Policy", "policy-deny.js"); + + return ShrinkWrap.create(JavaArchive.class, SCRIPT_DEPLOYMENT_NAME) + .addAsManifestResource(new StringAsset(JsonSerialization.writeValueAsPrettyString(representation)), + "keycloak-scripts.json") + .addAsResource(new StringAsset("$evaluation.grant();"), "policy-grant.js") + .addAsResource(new StringAsset("$evaluation.deny();"), "policy-deny.js"); + } + + @BeforeClass + public static void verifyEnvironment() { + ContainerAssume.assumeNotAuthServerUndertow(); + } + @ArquillianResource + private Deployer deployer; + + @Override + public void addTestRealms(List testRealms) { + testRealms.add(RealmBuilder.create().name("authz-test") + .roles(RolesBuilder.create().realmRole(RoleBuilder.create().name("uma_authorization").build())) + .user(UserBuilder.create().username("marta").password("password").addRoles("uma_authorization")) + .user(UserBuilder.create().username("kolo").password("password")) + .client(ClientBuilder.create().clientId("resource-server") + .secret("secret") + .authorizationServicesEnabled(true) + .redirectUris("http://localhost/resource-server-test") + .defaultRoles("uma_protection") + .directAccessGrants()) + .build()); + } + + @Before + public void onBefore() { + deployer.deploy(SCRIPT_DEPLOYMENT_NAME); + AuthorizationResource authorization = getAuthorizationResource(); + authorization.resources().create(new ResourceRepresentation("Default Resource")); + } + + @After + public void onAfter() { + deployer.undeploy(SCRIPT_DEPLOYMENT_NAME); + } + + @Test + public void testJSPolicyProviderNotAvailable() { + ProfileAssume.assumeFeatureDisabled(UPLOAD_SCRIPTS); + assertFalse(getAuthorizationResource().policies().policyProviders().stream().anyMatch(rep -> "js".equals(rep.getType()))); + } + + @Test + @UncaughtServerErrorExpected + public void failCreateJSPolicy() { + ProfileAssume.assumeFeatureDisabled(UPLOAD_SCRIPTS); + JSPolicyRepresentation grantPolicy = new JSPolicyRepresentation(); + + grantPolicy.setName("JS Policy"); + grantPolicy.setType("js"); + grantPolicy.setCode("$evaluation.grant();"); + + try (Response response = getAuthorizationResource().policies().js().create(grantPolicy)) { + assertEquals(500, response.getStatus()); + } + } + + @Test + public void testCreatePermission() { + AuthorizationResource authorization = getAuthorizationResource(); + PolicyRepresentation grantPolicy = new PolicyRepresentation(); + + grantPolicy.setName("Grant Policy"); + grantPolicy.setType("script-policy-grant.js"); + + authorization.policies().create(grantPolicy).close(); + + PolicyRepresentation denyPolicy = new PolicyRepresentation(); + + denyPolicy.setName("Deny Policy"); + denyPolicy.setType("script-policy-deny.js"); + + authorization.policies().create(denyPolicy).close(); + + PermissionsResource permissions = authorization.permissions(); + + ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation(); + + permission.setName("Test Deployed JS Permission"); + permission.addResource("Default Resource"); + permission.addPolicy(grantPolicy.getName()); + + permissions.resource().create(permission).close(); + + PolicyEvaluationRequest request = new PolicyEvaluationRequest(); + + request.setUserId("marta"); + request.addResource("Default Resource"); + + PolicyEvaluationResponse response = authorization.policies().evaluate(request); + + assertEquals(DecisionEffect.PERMIT, response.getStatus()); + + permission = permissions.resource().findByName(permission.getName()); + + permission.addPolicy(denyPolicy.getName()); + + permissions.resource().findById(permission.getId()).update(permission); + + response = authorization.policies().evaluate(request); + + assertEquals(DecisionEffect.DENY, response.getStatus()); + + permission.addPolicy(grantPolicy.getName()); + + permissions.resource().findById(permission.getId()).update(permission); + + response = authorization.policies().evaluate(request); + + assertEquals(DecisionEffect.DENY, response.getStatus()); + + permission.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE); + + permissions.resource().findById(permission.getId()).update(permission); + + response = authorization.policies().evaluate(request); + + assertEquals(DecisionEffect.PERMIT, response.getStatus()); + } + + private AuthorizationResource getAuthorizationResource() { + return getClient(realmsResouce().realm("authz-test"), "resource-server").authorization(); + } + + private ClientResource getClient(RealmResource realm, String clientId) { + ClientsResource clients = realm.clients(); + return clients.findByClientId(clientId).stream().map(representation -> clients.get(representation.getId())).findFirst() + .orElseThrow(() -> new RuntimeException("Expected client [resource-server-test]")); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/mapper-example.js b/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/mapper-example.js new file mode 100644 index 00000000000..fe6619e10d8 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/mapper-example.js @@ -0,0 +1 @@ +'hello_' + user.username \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/AggregatePolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/AggregatePolicyManagementTest.java index 719748973e2..5dee61a9de9 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/AggregatePolicyManagementTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/AggregatePolicyManagementTest.java @@ -19,6 +19,7 @@ package org.keycloak.testsuite.console.authorization; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.keycloak.common.Profile.Feature.UPLOAD_SCRIPTS; import java.util.UUID; @@ -52,6 +53,11 @@ import org.keycloak.testsuite.util.UserBuilder; */ public class AggregatePolicyManagementTest extends AbstractAuthorizationSettingsTest { + @Before + public void onBefore() { + enableFeature(UPLOAD_SCRIPTS); + } + @Before public void configureTest() { super.configureTest(); diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/JSPolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/JSPolicyManagementTest.java index 6da809c0722..535ab6dd379 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/JSPolicyManagementTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/JSPolicyManagementTest.java @@ -18,7 +18,11 @@ package org.keycloak.testsuite.console.authorization; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.keycloak.common.Profile.Feature.UPLOAD_SCRIPTS; +import javax.ws.rs.core.Response; + +import org.junit.Before; import org.junit.Test; import org.keycloak.representations.idm.authorization.JSPolicyRepresentation; import org.keycloak.representations.idm.authorization.Logic; @@ -29,6 +33,11 @@ import org.keycloak.testsuite.console.page.clients.authorization.policy.JSPolicy */ public class JSPolicyManagementTest extends AbstractAuthorizationSettingsTest { + @Before + public void onBefore() { + enableFeature(UPLOAD_SCRIPTS); + } + @Test public void testUpdate() throws InterruptedException { authorizationPage.navigateTo(); diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js index fbd53798f4b..24e0152fcf2 100644 --- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js @@ -354,6 +354,9 @@ module.config(['$routeProvider', function ($routeProvider) { }, client : function(ClientLoader) { return ClientLoader(); + }, + serverInfo : function(ServerInfoLoader) { + return ServerInfoLoader(); } }, controller: 'ResourceServerPolicyJSDetailCtrl' @@ -365,6 +368,9 @@ module.config(['$routeProvider', function ($routeProvider) { }, client : function(ClientLoader) { return ClientLoader(); + }, + serverInfo : function(ServerInfoLoader) { + return ServerInfoLoader(); } }, controller: 'ResourceServerPolicyJSDetailCtrl' diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js index caba91d27f4..703752cf0d8 100644 --- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js @@ -717,7 +717,14 @@ module.controller('ResourceServerPolicyCtrl', function($scope, $http, $route, $l }); $scope.addPolicy = function(policyType) { - $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/policy/" + policyType.type + "/create"); + if (policyType.type.endsWith('.js')) { + ResourceServerPolicy.save({realm : realm.realm, client : client.id, type: policyType.type}, {name: policyType.name, type: policyType.type}, function(data) { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/policy/"); + Notifications.success("The policy has been created."); + }); + } else { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/policy/" + policyType.type + "/create"); + } } $scope.firstPage = function() { @@ -1953,15 +1960,17 @@ module.controller('ResourceServerPolicyGroupDetailCtrl', function($scope, $route }, realm, client, $scope); }); -module.controller('ResourceServerPolicyJSDetailCtrl', function($scope, $route, $location, realm, PolicyController, client) { +module.controller('ResourceServerPolicyJSDetailCtrl', function($scope, $route, $location, realm, PolicyController, client, serverInfo) { PolicyController.onInit({ getPolicyType : function() { return "js"; }, onInit : function() { + $scope.readOnly = !serverInfo.featureEnabled('UPLOAD_SCRIPTS'); $scope.initEditor = function(editor){ editor.$blockScrolling = Infinity; + editor.setReadOnly($scope.readOnly); var session = editor.getSession(); session.setMode('ace/mode/javascript'); }; diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-js-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-js-detail.html index 80f70e1b9ca..172c2b61435 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-js-detail.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-js-detail.html @@ -23,14 +23,14 @@

- +
{{:: 'authz-policy-name.tooltip' | translate}}
- +
{{:: 'authz-policy-description.tooltip' | translate}}
@@ -46,7 +46,7 @@
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html index ac790bb2595..d6f220f8bc8 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html @@ -76,7 +76,7 @@ - {{policy.name}} + {{policy.name}} {{policy.description}} {{policy.type}} diff --git a/wildfly/server-subsystem/pom.xml b/wildfly/server-subsystem/pom.xml index af533feffac..a655d6b927d 100755 --- a/wildfly/server-subsystem/pom.xml +++ b/wildfly/server-subsystem/pom.xml @@ -111,6 +111,16 @@ keycloak-server-spi-private provided + + org.keycloak + keycloak-server-spi + provided + + + org.keycloak + keycloak-authz-policy-common + provided + org.wildfly.core diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakProviderDeploymentProcessor.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakProviderDeploymentProcessor.java index bf74732dc7e..6db04e05c01 100644 --- a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakProviderDeploymentProcessor.java +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakProviderDeploymentProcessor.java @@ -48,6 +48,9 @@ public class KeycloakProviderDeploymentProcessor implements DeploymentUnitProces } KeycloakDeploymentInfo info = KeycloakProviderDependencyProcessor.getKeycloakProviderDeploymentInfo(deploymentUnit); + + ScriptProviderDeploymentProcessor.deploy(deploymentUnit, info); + if (info.isProvider()) { logger.infov("Deploying Keycloak provider: {0}", deploymentUnit.getName()); final Module module = deploymentUnit.getAttachment(Attachments.MODULE); diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ScriptProviderDeploymentProcessor.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ScriptProviderDeploymentProcessor.java new file mode 100644 index 00000000000..c920c2d1c0a --- /dev/null +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/ScriptProviderDeploymentProcessor.java @@ -0,0 +1,133 @@ +/* + * Copyright 2019 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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 java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; + +import org.jboss.as.server.deployment.Attachments; +import org.jboss.as.server.deployment.DeploymentUnit; +import org.jboss.as.server.deployment.module.ResourceRoot; +import org.jboss.vfs.VirtualFile; +import org.keycloak.authentication.AuthenticatorSpi; +import org.keycloak.authentication.authenticators.browser.DeployedScriptAuthenticatorFactory; +import org.keycloak.authorization.policy.provider.PolicySpi; +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.provider.KeycloakDeploymentInfo; +import org.keycloak.representations.provider.ScriptProviderDescriptor; +import org.keycloak.representations.provider.ScriptProviderMetadata; +import org.keycloak.util.JsonSerialization; + +/** + * @author Pedro Igor + */ +final class ScriptProviderDeploymentProcessor { + + private static final Map> PROVIDERS = new HashMap<>(); + + private static void registerScriptAuthenticator(KeycloakDeploymentInfo info, ScriptProviderMetadata metadata) { + info.addProvider(AuthenticatorSpi.class, new DeployedScriptAuthenticatorFactory(metadata)); + } + + private static void registerScriptPolicy(KeycloakDeploymentInfo info, ScriptProviderMetadata metadata) { + info.addProvider(PolicySpi.class, new DeployedScriptPolicyFactory(metadata)); + } + + private static void registerScriptMapper(KeycloakDeploymentInfo info, ScriptProviderMetadata metadata) { + info.addProvider(ProtocolMapperSpi.class, new DeployedScriptOIDCProtocolMapper(metadata)); + } + + static void deploy(DeploymentUnit deploymentUnit, KeycloakDeploymentInfo info) { + ResourceRoot resourceRoot = deploymentUnit.getAttachment(Attachments.DEPLOYMENT_ROOT); + + if (resourceRoot == null) { + return; + } + + VirtualFile jarFile = resourceRoot.getRoot(); + + if (jarFile == null || !jarFile.exists() || !jarFile.getName().endsWith(".jar")) { + return; + } + + ScriptProviderDescriptor descriptor = readScriptProviderDescriptor(jarFile); + + if (descriptor == null) { + return; + } + + for (Map.Entry> entry : descriptor.getProviders().entrySet()) { + for (ScriptProviderMetadata metadata : entry.getValue()) { + String fileName = metadata.getFileName(); + + if (fileName == null) { + throw new RuntimeException("You must provide the script file name"); + } + + try (InputStream in = jarFile.getChild(fileName).openStream()) { + metadata.setCode(StreamUtil.readString(in, StandardCharsets.UTF_8)); + } catch (IOException cause) { + throw new RuntimeException("Failed to read script file [" + fileName + "]", cause); + } + + metadata.setId(new StringBuilder("script").append("-").append(fileName).toString()); + + String name = metadata.getName(); + + if (name == null) { + name = fileName; + } + + metadata.setName(name); + + PROVIDERS.get(entry.getKey()).accept(info, metadata); + } + } + } + + private static ScriptProviderDescriptor readScriptProviderDescriptor(VirtualFile deploymentRoot) { + VirtualFile metadataFile = deploymentRoot.getChild("META-INF/keycloak-scripts.json"); + + if (!metadataFile.exists()) { + return null; + } + + try (InputStream inputStream = metadataFile.openStream()) { + return JsonSerialization.readValue(inputStream, ScriptProviderDescriptor.class); + } catch (IOException cause) { + throw new RuntimeException("Failed to read providers metadata", cause); + } + } + + static { + PROVIDERS.put(AUTHENTICATORS, ScriptProviderDeploymentProcessor::registerScriptAuthenticator); + PROVIDERS.put(POLICIES, ScriptProviderDeploymentProcessor::registerScriptPolicy); + PROVIDERS.put(MAPPERS, ScriptProviderDeploymentProcessor::registerScriptMapper); + } +}