diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-subsystem/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-subsystem/main/module.xml new file mode 100644 index 0000000000..06e1229a40 --- /dev/null +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-subsystem/main/module.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java index 52eb239bdd..6be2346f59 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java @@ -11,6 +11,7 @@ import io.undertow.servlet.api.AuthMethodConfig; import io.undertow.servlet.api.DeploymentInfo; import io.undertow.servlet.api.LoginConfig; import io.undertow.servlet.api.ServletSessionConfig; +import java.io.ByteArrayInputStream; import org.jboss.logging.Logger; import org.keycloak.adapters.config.RealmConfiguration; import org.keycloak.representations.adapters.config.AdapterConfig; @@ -25,6 +26,11 @@ import java.util.Map; * @version $Revision: 1 $ */ public class KeycloakServletExtension implements ServletExtension { + // This param name is defined again in Keycloak Subsystem class + // org.keycloak.subsystem.extensionKeycloakAdapterConfigDeploymentProcessor. We have this value in + // two places to avoid dependency between Keycloak Subsystem and Keyclaok Undertow Integration. + public static final String AUTH_DATA_PARAM_NAME = "org.keycloak.json.adapterConfig"; + protected Logger log = Logger.getLogger(KeycloakServletExtension.class); // todo when this DeploymentInfo method of the same name is fixed. @@ -40,6 +46,13 @@ public class KeycloakServletExtension implements ServletExtension { return false; } + private InputStream getJSONFromServletContext(ServletContext servletContext) { + String json = servletContext.getInitParameter(AUTH_DATA_PARAM_NAME); + if (json == null) { + return null; + } + return new ByteArrayInputStream(json.getBytes()); + } @Override public void handleDeployment(DeploymentInfo deploymentInfo, ServletContext servletContext) { @@ -49,7 +62,10 @@ public class KeycloakServletExtension implements ServletExtension { } log.info("KeycloakServletException initialization"); InputStream is = servletContext.getResourceAsStream("/WEB-INF/keycloak.json"); - if (is == null) throw new RuntimeException("Unable to find /WEB-INF/keycloak.json configuration file"); + if (is == null) { + is = getJSONFromServletContext(servletContext); + } + if (is == null) throw new RuntimeException("Unable to find realm config in /WEB-INF/keycloak.json or in keycloak subsystem."); RealmConfigurationLoader loader = new RealmConfigurationLoader(is); loader.init(true); AdapterConfig keycloakConfig = loader.getAdapterConfig(); diff --git a/pom.xml b/pom.xml index 4b9409465b..533f92f4a5 100755 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,7 @@ 2.5.0.Beta6 2.11.2 3.1.1.GA + 1.2.0.Beta1 1.0.1.Final 4.0.1.Final 1.3.161 @@ -87,6 +88,7 @@ examples testsuite server + subsystem @@ -252,7 +254,7 @@ json ${json.version} - + com.google.http-client @@ -264,14 +266,14 @@ google-api-services-oauth2 v2-rev35-1.14.1-beta - + org.twitter4j twitter4j-core 3.0.5 - + com.google.zxing @@ -283,7 +285,7 @@ javase 2.2 - + com.icegreen @@ -340,6 +342,38 @@ ${keycloak.apache.httpcomponents.version} --> + + org.wildfly + wildfly-controller + ${wildfly.version} + + + org.wildfly + wildfly-server + ${wildfly.version} + + + org.wildfly + wildfly-ee + ${wildfly.version} + + + org.wildfly + wildfly-subsystem-test + ${wildfly.version} + pom + test + + + org.wildfly + wildfly-undertow + ${wildfly.version} + + + org.jboss.logging + jboss-logging-processor + ${jboss-logging-tools.version} + diff --git a/subsystem/pom.xml b/subsystem/pom.xml new file mode 100644 index 0000000000..d976b175d0 --- /dev/null +++ b/subsystem/pom.xml @@ -0,0 +1,115 @@ + + + + 4.0.0 + + + org.keycloak + keycloak-parent + 1.0-alpha-2-SNAPSHOT + + + org.keycloak + keycloak-subsystem + 1.0-alpha-2-SNAPSHOT + + Keycloak Subsystem + + jar + + + + + maven-compiler-plugin + 2.3.1 + + 1.6 + 1.6 + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.8.1 + + false + true + -Xmx512m + + + jboss.home + ${jboss.home} + + + + **/*TestCase.java + + once + + + + + + + + org.wildfly + wildfly-controller + + + org.wildfly + wildfly-server + + + org.wildfly + wildfly-ee + + + org.wildfly + wildfly-undertow + + + org.jboss.logging + jboss-logging-annotations + ${jboss-logging-tools.version} + + provided + true + + + + org.jboss.logging + jboss-logging-processor + + provided + true + + + + org.wildfly + wildfly-subsystem-test + pom + test + + + junit + junit + test + + + diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/CredentialAddHandler.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/CredentialAddHandler.java new file mode 100644 index 0000000000..ea14dfb4b8 --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/CredentialAddHandler.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import java.util.List; +import org.jboss.as.controller.AbstractAddStepHandler; +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.as.controller.ServiceVerificationHandler; +import org.jboss.dmr.ModelNode; +import org.jboss.msc.service.ServiceController; + +/** + * Add a credential to a deployment. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. + */ +public class CredentialAddHandler extends AbstractAddStepHandler { + + public CredentialAddHandler(AttributeDefinition... attributes) { + super(attributes); + } + + @Override + protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model, ServiceVerificationHandler verificationHandler, List> newControllers) throws OperationFailedException { + KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.find(context); + ckService.addCredential(operation, model); + } + +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/CredentialDefinition.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/CredentialDefinition.java new file mode 100644 index 0000000000..9ae3abdfb6 --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/CredentialDefinition.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.ModelOnlyWriteAttributeHandler; +import org.jboss.as.controller.PathElement; +import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; +import org.jboss.as.controller.SimpleResourceDefinition; +import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler; +import org.jboss.as.controller.operations.validation.StringLengthValidator; +import org.jboss.as.controller.registry.ManagementResourceRegistration; +import org.jboss.dmr.ModelType; + +/** + * Defines attributes and operations for a credential. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +public class CredentialDefinition extends SimpleResourceDefinition { + + public static final String TAG_NAME = "credential"; + + protected static final AttributeDefinition VALUE = + new SimpleAttributeDefinitionBuilder("value", ModelType.STRING, false) + .setXmlName("value") + .setAllowExpression(true) + .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, false, true)) + .build(); + + public CredentialDefinition() { + super(PathElement.pathElement(TAG_NAME), + KeycloakExtension.getResourceDescriptionResolver(TAG_NAME), + new CredentialAddHandler(VALUE), + CredentialRemoveHandler.INSTANCE); + } + + @Override + public void registerOperations(ManagementResourceRegistration resourceRegistration) { + super.registerOperations(resourceRegistration); + resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE); + } + + @Override + public void registerAttributes(ManagementResourceRegistration resourceRegistration) { + super.registerAttributes(resourceRegistration); + resourceRegistration.registerReadWriteAttribute(VALUE, null, new CredentialReadWriteAttributeHandler()); + } +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/CredentialReadWriteAttributeHandler.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/CredentialReadWriteAttributeHandler.java new file mode 100644 index 0000000000..6289ff4539 --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/CredentialReadWriteAttributeHandler.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import org.jboss.as.controller.AbstractWriteAttributeHandler; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.dmr.ModelNode; + +/** + * Update a credential value. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. + */ +public class CredentialReadWriteAttributeHandler extends AbstractWriteAttributeHandler { + + @Override + protected boolean applyUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName, + ModelNode resolvedValue, ModelNode currentValue, AbstractWriteAttributeHandler.HandbackHolder hh) throws OperationFailedException { + + KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.find(context); + ckService.updateCredential(operation, attributeName, resolvedValue); + + hh.setHandback(ckService); + + return false; + } + + @Override + protected void revertUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName, + ModelNode valueToRestore, ModelNode valueToRevert, KeycloakAdapterConfigService ckService) throws OperationFailedException { + ckService.updateCredential(operation, attributeName, valueToRestore); + } + +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/CredentialRemoveHandler.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/CredentialRemoveHandler.java new file mode 100644 index 0000000000..9d1d698df8 --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/CredentialRemoveHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import org.jboss.as.controller.AbstractRemoveStepHandler; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.dmr.ModelNode; + +/** + * Remove a credential from a deployment. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. + */ +public class CredentialRemoveHandler extends AbstractRemoveStepHandler { + + public static CredentialRemoveHandler INSTANCE = new CredentialRemoveHandler(); + + private CredentialRemoveHandler() {} + + @Override + protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { + KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.find(context); + ckService.removeCredential(operation); + } + +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakAdapterConfigDeploymentProcessor.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakAdapterConfigDeploymentProcessor.java new file mode 100644 index 0000000000..17d73841d7 --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakAdapterConfigDeploymentProcessor.java @@ -0,0 +1,112 @@ +/* + * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import java.util.ArrayList; +import java.util.List; +import org.jboss.as.server.deployment.DeploymentPhaseContext; +import org.jboss.as.server.deployment.DeploymentUnit; +import org.jboss.as.server.deployment.DeploymentUnitProcessingException; +import org.jboss.as.server.deployment.DeploymentUnitProcessor; +import org.jboss.as.server.deployment.Phase; +import org.jboss.as.web.common.WarMetaData; +import org.jboss.metadata.javaee.spec.ParamValueMetaData; +import org.jboss.metadata.web.jboss.JBossWebMetaData; + +/** + * Pass authentication data (keycloak.json) as a servlet context param so it can be read by the KeycloakServletExtension. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. + */ +public class KeycloakAdapterConfigDeploymentProcessor implements DeploymentUnitProcessor { + // This param name is defined again in Keycloak Undertow Integration class + // org.keycloak.adapters.undertow.KeycloakServletExtension. We have this value in + // two places to avoid dependency between Keycloak Subsystem and Keyclaok Undertow Integration. + public static final String AUTH_DATA_PARAM_NAME = "org.keycloak.json.adapterConfig"; + + public static final Phase PHASE = Phase.INSTALL; + // Seems wise to have this run after INSTALL_WAR_DEPLOYMENT + public static final int PRIORITY = Phase.INSTALL_WAR_DEPLOYMENT + 1; + + @Override + public void deploy(DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException { + DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit(); + String deploymentName = deploymentUnit.getName(); + + KeycloakAdapterConfigService service = KeycloakAdapterConfigService.find(phaseContext.getServiceRegistry()); + if (service.isKeycloakDeployment(deploymentName)) { + addKeycloakAuthData(phaseContext, deploymentName, service); + } + } + + private void addKeycloakAuthData(DeploymentPhaseContext phaseContext, String deploymentName, KeycloakAdapterConfigService service) { + DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit(); + WarMetaData warMetaData = deploymentUnit.getAttachment(WarMetaData.ATTACHMENT_KEY); + + //TODO: Find context root properly + String resourceName = deploymentName.substring(0, deploymentName.lastIndexOf('.')); + + addJSONData(service.getJSON(deploymentName, resourceName), warMetaData); + //addJSONData(getJSON(), warMetaData); + } + + // TODO: remove this. + private String getJSON() { + return "{\n" + +" \"realm\": \"demo\",\n" + +" \"realm-public-key\": \"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB\",\n" + +" \"auth-server-url\": \"http://localhost:8080/auth\",\n" + +" \"ssl-not-required\": true,\n" + +" \"resource\": \"customer-portal-subsys\",\n" + +" \"credentials\": {\n" + +" \"password\": \"password\"\n" + +" },\n" + +" \"use-resource-role-mappings\": false,\n" + +" \"enable-cors\": false,\n" + +" \"cors-max-age\": -1,\n" + +" \"expose-token\": false,\n" + +" \"bearer-only\": false\n" + +"}"; + } + + private void addJSONData(String json, WarMetaData warMetaData) { + JBossWebMetaData webMetaData = warMetaData.getMergedJBossWebMetaData(); + if (webMetaData == null) { + webMetaData = new JBossWebMetaData(); + warMetaData.setMergedJBossWebMetaData(webMetaData); + } + + List contextParams = webMetaData.getContextParams(); + if (contextParams == null) { + contextParams = new ArrayList(); + } + + ParamValueMetaData param = new ParamValueMetaData(); + param.setParamName(AUTH_DATA_PARAM_NAME); + param.setParamValue(json); + contextParams.add(param); + + webMetaData.setContextParams(contextParams); + } + + @Override + public void undeploy(DeploymentUnit du) { + + } + +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakAdapterConfigService.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakAdapterConfigService.java new file mode 100644 index 0000000000..11f71b2dac --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakAdapterConfigService.java @@ -0,0 +1,207 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import java.util.HashMap; +import java.util.Map; +import org.jboss.as.controller.OperationContext; +import org.jboss.dmr.ModelNode; +import org.jboss.dmr.Property; +import org.jboss.msc.service.Service; +import org.jboss.msc.service.ServiceController; +import org.jboss.msc.service.ServiceName; +import org.jboss.msc.service.ServiceRegistry; +import org.jboss.msc.service.StartContext; +import org.jboss.msc.service.StartException; +import org.jboss.msc.service.StopContext; + +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADDRESS; + +/** + * This service keeps track of the entire Keycloak management model so as to provide + * adapter configuration to each deployment at deploy time. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +public class KeycloakAdapterConfigService implements Service { + private static final String CREDENTIALS_JSON_NAME = "credentials"; + + // Right now this is used as a service, but I'm not sure it really needs to be implemented that way. + // It's also a singleton serving the entire subsystem, but the INSTANCE variable is currently only + // used during initialization of the subsystem. + public static final ServiceName SERVICE_NAME = ServiceName.JBOSS.append("KeycloakAdapterConfigService"); + public static final KeycloakAdapterConfigService INSTANCE = new KeycloakAdapterConfigService(); + + private Map realms = new HashMap(); + private Map deployments = new HashMap(); + + private KeycloakAdapterConfigService() { + + } + + @Override + public void start(StartContext sc) throws StartException { + + } + + @Override + public void stop(StopContext sc) { + + } + + @Override + public KeycloakAdapterConfigService getValue() throws IllegalStateException, IllegalArgumentException { + return this; + } + + public void addRealm(ModelNode operation, ModelNode model) { + this.realms.put(realmNameFromOp(operation), model.clone()); + } + + public void updateRealm(ModelNode operation, String attrName, ModelNode resolvedValue) { + ModelNode realm = this.realms.get(realmNameFromOp(operation)); + realm.get(attrName).set(resolvedValue); + } + + public void removeRealm(ModelNode operation) { + this.realms.remove(realmNameFromOp(operation)); + } + + public void addSecureDeployment(ModelNode operation, ModelNode model) { + ModelNode deployment = model.clone(); + deployment.get(RealmDefinition.TAG_NAME).set(realmNameFromOp(operation)); + this.deployments.put(deploymentNameFromOp(operation), deployment); + } + + public void updateSecureDeployment(ModelNode operation, String attrName, ModelNode resolvedValue) { + ModelNode deployment = this.deployments.get(deploymentNameFromOp(operation)); + deployment.get(attrName).set(resolvedValue); + } + + public void removeSecureDeployment(ModelNode operation) { + this.deployments.remove(deploymentNameFromOp(operation)); + } + + public void addCredential(ModelNode operation, ModelNode model) { + ModelNode credentials = credentialsFromOp(operation); + if (!credentials.isDefined()) { + credentials = new ModelNode(); + } + + String credentialName = credentialNameFromOp(operation); + credentials.get(credentialName).set(model.get("value").asString()); + + ModelNode deployment = this.deployments.get(deploymentNameFromOp(operation)); + deployment.get(CREDENTIALS_JSON_NAME).set(credentials); + } + + public void removeCredential(ModelNode operation) { + ModelNode credentials = credentialsFromOp(operation); + if (!credentials.isDefined()) { + throw new RuntimeException("Can not remove credential. No credential defined for deployment in op " + operation.toString()); + } + + String credentialName = credentialNameFromOp(operation); + credentials.remove(credentialName); + } + + public void updateCredential(ModelNode operation, String attrName, ModelNode resolvedValue) { + ModelNode credentials = credentialsFromOp(operation); + if (!credentials.isDefined()) { + throw new RuntimeException("Can not update credential. No credential defined for deployment in op " + operation.toString()); + } + + String credentialName = credentialNameFromOp(operation); + credentials.get(credentialName).set(resolvedValue); + } + + private ModelNode credentialsFromOp(ModelNode operation) { + ModelNode deployment = this.deployments.get(deploymentNameFromOp(operation)); + return deployment.get(CREDENTIALS_JSON_NAME); + } + + private String realmNameFromOp(ModelNode operation) { + return valueFromOpAddress(RealmDefinition.TAG_NAME, operation); + } + + private String deploymentNameFromOp(ModelNode operation) { + return valueFromOpAddress(SecureDeploymentDefinition.TAG_NAME, operation); + } + + private String credentialNameFromOp(ModelNode operation) { + return valueFromOpAddress(CredentialDefinition.TAG_NAME, operation); + } + + private String valueFromOpAddress(String addrElement, ModelNode operation) { + String deploymentName = getValueOfAddrElement(operation.get(ADDRESS), addrElement); + if (deploymentName == null) throw new RuntimeException("Can't find '" + addrElement + "' in address " + operation.toString()); + return deploymentName; + } + + private String getValueOfAddrElement(ModelNode address, String elementName) { + for (ModelNode element : address.asList()) { + if (element.has(elementName)) return element.get(elementName).asString(); + } + + return null; + } + + public String getJSON(String deploymentName, String resourceName) { + ModelNode deployment = this.deployments.get(deploymentName); + String realmName = deployment.get(RealmDefinition.TAG_NAME).asString(); + ModelNode realm = this.realms.get(realmName); + + ModelNode json = new ModelNode(); + json.get(RealmDefinition.TAG_NAME).set(realmName); + json.get("resource").set(resourceName); + + // Realm values set first. Some can be overridden by deployment values. + setJSONValues(json, realm); + setJSONValues(json, deployment); + + // TODO: change this to true to compact the string + return json.toJSONString(false); + } + + private void setJSONValues(ModelNode json, ModelNode values) { + for (Property prop : values.asPropertyList()) { + String name = prop.getName(); + ModelNode value = prop.getValue(); + if (value.isDefined()) { + json.get(name).set(value); + } + } + } + + public boolean isKeycloakDeployment(String deploymentName) { + return this.deployments.containsKey(deploymentName); + } + + static KeycloakAdapterConfigService find(ServiceRegistry registry) { + ServiceController container = registry.getService(KeycloakAdapterConfigService.SERVICE_NAME); + if (container != null) { + KeycloakAdapterConfigService service = (KeycloakAdapterConfigService)container.getValue(); + return service; + } + return null; + } + + static KeycloakAdapterConfigService find(OperationContext context) { + return find(context.getServiceRegistry(true)); + } +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakDependencyProcessor.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakDependencyProcessor.java new file mode 100644 index 0000000000..eb885b02c9 --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakDependencyProcessor.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import org.jboss.as.server.deployment.Attachments; +import org.jboss.as.server.deployment.DeploymentPhaseContext; +import org.jboss.as.server.deployment.DeploymentUnit; +import org.jboss.as.server.deployment.DeploymentUnitProcessingException; +import org.jboss.as.server.deployment.DeploymentUnitProcessor; +import org.jboss.as.server.deployment.module.ModuleDependency; +import org.jboss.as.server.deployment.module.ModuleSpecification; +import org.jboss.modules.Module; +import org.jboss.modules.ModuleIdentifier; +import org.jboss.modules.ModuleLoader; + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +public class KeycloakDependencyProcessor implements DeploymentUnitProcessor { + + private static final ModuleIdentifier KEYCLOAK_UNDERTOW_ADAPTER = ModuleIdentifier.create("org.keycloak.keycloak-undertow-adapter"); + private static final ModuleIdentifier KEYCLOAK_CORE_ADAPTER = ModuleIdentifier.create("org.keycloak.keycloak-adapter-core"); + private static final ModuleIdentifier APACHE_HTTPCOMPONENTS = ModuleIdentifier.create("org.apache.httpcomponents"); + + @Override + public void deploy(DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException { + final DeploymentUnit deploymentUnit = phaseContext.getDeploymentUnit(); + + KeycloakAdapterConfigService service = KeycloakAdapterConfigService.find(phaseContext.getServiceRegistry()); + if (service.isKeycloakDeployment(deploymentUnit.getName())) { + addModules(deploymentUnit); + } + } + + private void addModules(DeploymentUnit deploymentUnit) { + final ModuleSpecification moduleSpecification = deploymentUnit.getAttachment(Attachments.MODULE_SPECIFICATION); + final ModuleLoader moduleLoader = Module.getBootModuleLoader(); + + moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_UNDERTOW_ADAPTER, false, false, true, false)); + moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_CORE_ADAPTER, false, false, false, false)); + moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, APACHE_HTTPCOMPONENTS, false, false, true, false)); + } + + @Override + public void undeploy(DeploymentUnit du) { + + } + +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakExtension.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakExtension.java new file mode 100644 index 0000000000..ed953d1e52 --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakExtension.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import org.jboss.as.controller.Extension; +import org.jboss.as.controller.ExtensionContext; +import org.jboss.as.controller.PathElement; +import org.jboss.as.controller.SubsystemRegistration; +import org.jboss.as.controller.descriptions.StandardResourceDescriptionResolver; +import org.jboss.as.controller.parsing.ExtensionParsingContext; +import org.jboss.as.controller.registry.ManagementResourceRegistration; +import org.jboss.as.controller.ResourceDefinition; +import org.keycloak.subsystem.logging.KeycloakLogger; + +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM; + + +/** + * Main Extension class for the subsystem. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +public class KeycloakExtension implements Extension { + + public static final String SUBSYSTEM_NAME = "keycloak"; + public static final String NAMESPACE = "urn:jboss:domain:keycloak:1.0"; + private static final KeycloakSubsystemParser PARSER = new KeycloakSubsystemParser(); + static final PathElement PATH_SUBSYSTEM = PathElement.pathElement(SUBSYSTEM, SUBSYSTEM_NAME); + private static final String RESOURCE_NAME = KeycloakExtension.class.getPackage().getName() + ".LocalDescriptions"; + private static final int MANAGEMENT_API_MAJOR_VERSION = 1; + private static final int MANAGEMENT_API_MINOR_VERSION = 0; + private static final int MANAGEMENT_API_MICRO_VERSION = 0; + protected static final PathElement SUBSYSTEM_PATH = PathElement.pathElement(SUBSYSTEM, SUBSYSTEM_NAME); + private static final ResourceDefinition KEYCLOAK_SUBSYSTEM_RESOURCE = new KeycloakSubsystemDefinition(); + static final RealmDefinition REALM_DEFINITION = new RealmDefinition(); + static final SecureDeploymentDefinition SECURE_DEPLOYMENT_DEFINITION = new SecureDeploymentDefinition(); + static final CredentialDefinition CREDENTIAL_DEFINITION = new CredentialDefinition(); + + static StandardResourceDescriptionResolver getResourceDescriptionResolver(final String... keyPrefix) { + StringBuilder prefix = new StringBuilder(SUBSYSTEM_NAME); + for (String kp : keyPrefix) { + prefix.append('.').append(kp); + } + return new StandardResourceDescriptionResolver(prefix.toString(), RESOURCE_NAME, KeycloakExtension.class.getClassLoader(), true, false); + } + + /** + * {@inheritDoc} + */ + @Override + public void initializeParsers(final ExtensionParsingContext context) { + context.setSubsystemXmlMapping(SUBSYSTEM_NAME, KeycloakExtension.NAMESPACE, PARSER); + } + + /** + * {@inheritDoc} + */ + @Override + public void initialize(final ExtensionContext context) { + KeycloakLogger.ROOT_LOGGER.debug("Activating Keycloak Extension"); + final SubsystemRegistration subsystem = context.registerSubsystem(SUBSYSTEM_NAME, MANAGEMENT_API_MAJOR_VERSION, + MANAGEMENT_API_MINOR_VERSION, MANAGEMENT_API_MICRO_VERSION); + + ManagementResourceRegistration registration = subsystem.registerSubsystemModel(KEYCLOAK_SUBSYSTEM_RESOURCE); + ManagementResourceRegistration realmRegistration = registration.registerSubModel(REALM_DEFINITION); + ManagementResourceRegistration secureDeploymentRegistration = realmRegistration.registerSubModel(SECURE_DEPLOYMENT_DEFINITION); + secureDeploymentRegistration.registerSubModel(CREDENTIAL_DEFINITION); + + subsystem.registerXMLElementWriter(PARSER); + } +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakSubsystemAdd.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakSubsystemAdd.java new file mode 100644 index 0000000000..846a7701af --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakSubsystemAdd.java @@ -0,0 +1,75 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import java.util.List; + +import org.jboss.as.controller.AbstractBoottimeAddStepHandler; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.as.controller.ServiceVerificationHandler; +import org.jboss.as.server.AbstractDeploymentChainStep; +import org.jboss.as.server.DeploymentProcessorTarget; +import org.jboss.as.server.deployment.Phase; +import org.jboss.dmr.ModelNode; +import org.jboss.msc.service.ServiceController; + +/** + * The Keycloak subsystem add update handler. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +class KeycloakSubsystemAdd extends AbstractBoottimeAddStepHandler { + + static final KeycloakSubsystemAdd INSTANCE = new KeycloakSubsystemAdd(); + + @Override + protected void populateModel(ModelNode operation, ModelNode model) throws OperationFailedException { + model.setEmptyObject(); + } + + @Override + protected void performBoottime(final OperationContext context, ModelNode operation, final ModelNode model, ServiceVerificationHandler verificationHandler, List> newControllers) { + context.addStep(new AbstractDeploymentChainStep() { + @Override + protected void execute(DeploymentProcessorTarget processorTarget) { + processorTarget.addDeploymentProcessor(KeycloakExtension.SUBSYSTEM_NAME, Phase.DEPENDENCIES, 0, new KeycloakDependencyProcessor()); + processorTarget.addDeploymentProcessor(KeycloakExtension.SUBSYSTEM_NAME, + KeycloakAdapterConfigDeploymentProcessor.PHASE, + KeycloakAdapterConfigDeploymentProcessor.PRIORITY, + new KeycloakAdapterConfigDeploymentProcessor()); + } + }, OperationContext.Stage.RUNTIME); + } + + @Override + protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model, ServiceVerificationHandler verificationHandler, List> newControllers) throws OperationFailedException { + super.performRuntime(context, operation, model, verificationHandler, newControllers); + + ServiceController controller = context.getServiceTarget() + .addService(KeycloakAdapterConfigService.SERVICE_NAME, KeycloakAdapterConfigService.INSTANCE) + .addListener(verificationHandler) + .setInitialMode(ServiceController.Mode.ACTIVE) + .install(); + newControllers.add(controller); + } + + @Override + protected boolean requiresRuntimeVerification() { + return false; + } +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakSubsystemDefinition.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakSubsystemDefinition.java new file mode 100644 index 0000000000..fe9b57b37a --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakSubsystemDefinition.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import org.jboss.as.controller.PathElement; +import org.jboss.as.controller.ReloadRequiredRemoveStepHandler; +import org.jboss.as.controller.SimpleResourceDefinition; +import org.jboss.as.controller.descriptions.ModelDescriptionConstants; +import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler; +import org.jboss.as.controller.registry.ManagementResourceRegistration; + +/** + * Definition of subsystem=keycloak. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +public class KeycloakSubsystemDefinition extends SimpleResourceDefinition { + protected KeycloakSubsystemDefinition() { + super(PathElement.pathElement(ModelDescriptionConstants.SUBSYSTEM, KeycloakExtension.SUBSYSTEM_NAME), + KeycloakExtension.getResourceDescriptionResolver("subsystem"), + KeycloakSubsystemAdd.INSTANCE, + ReloadRequiredRemoveStepHandler.INSTANCE + ); + } + + @Override + public void registerOperations(ManagementResourceRegistration resourceRegistration) { + super.registerOperations(resourceRegistration); + resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE); + } + +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakSubsystemParser.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakSubsystemParser.java new file mode 100644 index 0000000000..d4bd89bfe6 --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/KeycloakSubsystemParser.java @@ -0,0 +1,227 @@ +/* + * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.PathAddress; +import org.jboss.as.controller.PathElement; +import org.jboss.as.controller.SimpleAttributeDefinition; +import org.jboss.as.controller.descriptions.ModelDescriptionConstants; +import org.jboss.as.controller.operations.common.Util; +import org.jboss.as.controller.parsing.ParseUtils; +import org.jboss.as.controller.persistence.SubsystemMarshallingContext; +import org.jboss.dmr.ModelNode; +import org.jboss.dmr.Property; +import org.jboss.staxmapper.XMLElementReader; +import org.jboss.staxmapper.XMLElementWriter; +import org.jboss.staxmapper.XMLExtendedStreamReader; +import org.jboss.staxmapper.XMLExtendedStreamWriter; + +/** + * The subsystem parser, which uses stax to read and write to and from xml + */ +class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader>, XMLElementWriter { + + /** + * {@inheritDoc} + */ + @Override + public void readElement(final XMLExtendedStreamReader reader, final List list) throws XMLStreamException { + // Require no attributes + ParseUtils.requireNoAttributes(reader); + ModelNode addKeycloakSub = Util.createAddOperation(PathAddress.pathAddress(KeycloakExtension.PATH_SUBSYSTEM)); + list.add(addKeycloakSub); + + while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { + if (!reader.getLocalName().equals("realm")) { + throw ParseUtils.unexpectedElement(reader); + } + readRealm(reader, list); + } + } + + // used for debugging + private int nextTag(XMLExtendedStreamReader reader) throws XMLStreamException { + return reader.nextTag(); + } + + private void readRealm(XMLExtendedStreamReader reader, List list) throws XMLStreamException { + String realmName = readNameAttribute(reader); + ModelNode composite = new ModelNode(); + composite.get(ModelDescriptionConstants.OP_ADDR).setEmptyList(); + composite.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.COMPOSITE); + ModelNode addRealm = new ModelNode(); + addRealm.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.ADD); + PathAddress addr = PathAddress.pathAddress(PathElement.pathElement(ModelDescriptionConstants.SUBSYSTEM, KeycloakExtension.SUBSYSTEM_NAME), + PathElement.pathElement(RealmDefinition.TAG_NAME, realmName)); + addRealm.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode()); + + List resourcesToAdd = new ArrayList(); + while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { + String tagName = reader.getLocalName(); + if (tagName.equals(SecureDeploymentDefinition.TAG_NAME)) { + readDeployment(reader, addr, resourcesToAdd); + continue; + } + + SimpleAttributeDefinition def = RealmDefinition.lookup(tagName); + if (def == null) throw new XMLStreamException("Unknown realm tag " + tagName); + def.parseAndSetParameter(reader.getElementText(), addRealm, reader); + } + + if (!RealmDefinition.validateTruststoreSetIfRequired(addRealm)) { + //TODO: externalize the message + throw new XMLStreamException("truststore and truststore-password must be set if both ssl-not-required and disable-trust-maanger are false."); + } + + ModelNode steps = new ModelNode(); + steps.add(addRealm); + for (ModelNode resource : resourcesToAdd) { + steps.add(resource); + } + composite.get(ModelDescriptionConstants.STEPS).set(steps); + + list.add(composite); + } + + private void readDeployment(XMLExtendedStreamReader reader, PathAddress parent, List resourcesToAdd) throws XMLStreamException { + String name = readNameAttribute(reader); + ModelNode addSecureDeployment = new ModelNode(); + addSecureDeployment.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.ADD); + PathAddress addr = PathAddress.pathAddress(parent, PathElement.pathElement(SecureDeploymentDefinition.TAG_NAME, name)); + addSecureDeployment.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode()); + List credentialsToAdd = new ArrayList(); + while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { + String tagName = reader.getLocalName(); + if (tagName.equals(CredentialDefinition.TAG_NAME)) { + readCredential(reader, addr, credentialsToAdd); + continue; + } + + SimpleAttributeDefinition def = SecureDeploymentDefinition.lookup(tagName); + if (def == null) throw new XMLStreamException("Unknown secure-deployment tag " + tagName); + def.parseAndSetParameter(reader.getElementText(), addSecureDeployment, reader); + } + // Must add credentials after the deployment is added. + resourcesToAdd.add(addSecureDeployment); + resourcesToAdd.addAll(credentialsToAdd); + } + + public void readCredential(XMLExtendedStreamReader reader, PathAddress parent, List credentialsToAdd) throws XMLStreamException { + String name = readNameAttribute(reader); + ModelNode addCredential = new ModelNode(); + addCredential.get(ModelDescriptionConstants.OP).set(ModelDescriptionConstants.ADD); + PathAddress addr = PathAddress.pathAddress(parent, PathElement.pathElement(CredentialDefinition.TAG_NAME, name)); + addCredential.get(ModelDescriptionConstants.OP_ADDR).set(addr.toModelNode()); + addCredential.get(CredentialDefinition.VALUE.getName()).set(reader.getElementText()); + credentialsToAdd.add(addCredential); + } + + // expects that the current tag will have one single attribute called "name" + private String readNameAttribute(XMLExtendedStreamReader reader) throws XMLStreamException { + String name = null; + for (int i = 0; i < reader.getAttributeCount(); i++) { + String attr = reader.getAttributeLocalName(i); + if (attr.equals("name")) { + name = reader.getAttributeValue(i); + continue; + } + throw ParseUtils.unexpectedAttribute(reader, i); + } + if (name == null) { + throw ParseUtils.missingRequired(reader, Collections.singleton("name")); + } + return name; + } + + /** + * {@inheritDoc} + */ + @Override + public void writeContent(final XMLExtendedStreamWriter writer, final SubsystemMarshallingContext context) throws XMLStreamException { + context.startSubsystemElement(KeycloakExtension.NAMESPACE, false); + writeRealms(writer, context); + writer.writeEndElement(); + } + + private void writeRealms(XMLExtendedStreamWriter writer, SubsystemMarshallingContext context) throws XMLStreamException { + if (!context.getModelNode().get(RealmDefinition.TAG_NAME).isDefined()) { + return; + } + for (Property realm : context.getModelNode().get(RealmDefinition.TAG_NAME).asPropertyList()) { + writer.writeStartElement(RealmDefinition.TAG_NAME); + writer.writeAttribute("name", realm.getName()); + ModelNode realmElements = realm.getValue(); + for (AttributeDefinition element : RealmDefinition.ALL_ATTRIBUTES) { + element.marshallAsElement(realmElements, writer); + } + + ModelNode deployments = realmElements.get(SecureDeploymentDefinition.TAG_NAME); + if (deployments.isDefined()) { + writeSecureDeployments(writer, deployments); + } + + writer.writeEndElement(); + } + } + + private void writeSecureDeployments(XMLExtendedStreamWriter writer, ModelNode deployments) throws XMLStreamException { + for (Property deployment : deployments.asPropertyList()) { + writer.writeStartElement(SecureDeploymentDefinition.TAG_NAME); + writer.writeAttribute("name", deployment.getName()); + ModelNode deploymentElements = deployment.getValue(); + for (AttributeDefinition element : SecureDeploymentDefinition.ALL_ATTRIBUTES) { + element.marshallAsElement(deploymentElements, writer); + } + + ModelNode credentials = deploymentElements.get(CredentialDefinition.TAG_NAME); + if (credentials.isDefined()) { + writeCredentials(writer, credentials); + } + + writer.writeEndElement(); + } + } + + private void writeCredentials(XMLExtendedStreamWriter writer, ModelNode credentials) throws XMLStreamException { + for (Property credential : credentials.asPropertyList()) { + writer.writeStartElement(CredentialDefinition.TAG_NAME); + writer.writeAttribute("name", credential.getName()); + String credentialValue = credential.getValue().get(CredentialDefinition.VALUE.getName()).asString(); + writeCharacters(writer, credentialValue); + writer.writeEndElement(); + } + } + + // code taken from org.jboss.as.controller.AttributeMarshaller + private void writeCharacters(XMLExtendedStreamWriter writer, String content) throws XMLStreamException { + if (content.indexOf('\n') > -1) { + // Multiline content. Use the overloaded variant that staxmapper will format + writer.writeCharacters(content); + } else { + // Staxmapper will just output the chars without adding newlines if this is used + char[] chars = content.toCharArray(); + writer.writeCharacters(chars, 0, chars.length); + } + } + +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/RealmAddHandler.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/RealmAddHandler.java new file mode 100644 index 0000000000..9705980968 --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/RealmAddHandler.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import java.util.List; +import org.jboss.as.controller.AbstractAddStepHandler; +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.as.controller.ServiceVerificationHandler; +import org.jboss.dmr.ModelNode; + +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP; +import org.jboss.msc.service.ServiceController; + +/** + * Add a new realm. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +public class RealmAddHandler extends AbstractAddStepHandler { + + public static RealmAddHandler INSTANCE = new RealmAddHandler(); + + private RealmAddHandler() {} + + @Override + protected void populateModel(ModelNode operation, ModelNode model) throws OperationFailedException { + // TODO: localize exception. get id number + if (!operation.get(OP).asString().equals(ADD)) { + throw new OperationFailedException("Unexpected operation for add realm. operation=" + operation.toString()); + } + + for (AttributeDefinition attrib : RealmDefinition.ALL_ATTRIBUTES) { + attrib.validateAndSet(operation, model); + } + + if (!RealmDefinition.validateTruststoreSetIfRequired(model.clone())) { + //TODO: externalize message + throw new OperationFailedException("truststore and truststore-password must be set if both ssl-not-required and disable-trust-maanger are false."); + } + } + + @Override + protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model, ServiceVerificationHandler verificationHandler, List> newControllers) throws OperationFailedException { + KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.find(context); + ckService.addRealm(operation, model); + } +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/RealmDefinition.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/RealmDefinition.java new file mode 100644 index 0000000000..7ecdcb3fc5 --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/RealmDefinition.java @@ -0,0 +1,173 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.PathElement; +import org.jboss.as.controller.SimpleAttributeDefinition; +import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; +import org.jboss.as.controller.SimpleResourceDefinition; +import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler; +import org.jboss.as.controller.operations.validation.IntRangeValidator; +import org.jboss.as.controller.operations.validation.StringLengthValidator; +import org.jboss.as.controller.registry.ManagementResourceRegistration; +import org.jboss.dmr.ModelNode; +import org.jboss.dmr.ModelType; + +/** + * Defines attributes and operations for the Realm + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +public class RealmDefinition extends SimpleResourceDefinition { + + public static final String TAG_NAME = "realm"; + + protected static final SimpleAttributeDefinition REALM_PUBLIC_KEY = + new SimpleAttributeDefinitionBuilder("realm-public-key", ModelType.STRING, false) + .setXmlName("realm-public-key") + .setAllowExpression(true) + .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, false, true)) + .build(); + protected static final SimpleAttributeDefinition AUTH_SERVER_URL = + new SimpleAttributeDefinitionBuilder("auth-server-url", ModelType.STRING, false) + .setXmlName("auth-server-url") + .setAllowExpression(true) + .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, false, true)) + .build(); + protected static final SimpleAttributeDefinition SSL_NOT_REQUIRED = + new SimpleAttributeDefinitionBuilder("ssl-not-required", ModelType.BOOLEAN, true) + .setXmlName("ssl-not-required") + .setAllowExpression(true) + .setDefaultValue(new ModelNode(false)) + .build(); + protected static final SimpleAttributeDefinition ALLOW_ANY_HOSTNAME = + new SimpleAttributeDefinitionBuilder("allow-any-hostname", ModelType.BOOLEAN, true) + .setXmlName("allow-any-hostname") + .setAllowExpression(true) + .setDefaultValue(new ModelNode(false)) + .build(); + protected static final SimpleAttributeDefinition DISABLE_TRUST_MANAGER = + new SimpleAttributeDefinitionBuilder("disable-trust-manager", ModelType.BOOLEAN, true) + .setXmlName("disable-trust-manager") + .setAllowExpression(true) + .setDefaultValue(new ModelNode(false)) + .build(); + protected static final SimpleAttributeDefinition TRUSTSTORE = + new SimpleAttributeDefinitionBuilder("truststore", ModelType.STRING, true) + .setXmlName("truststore") + .setAllowExpression(true) + .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) + .build(); + protected static final SimpleAttributeDefinition TRUSTSTORE_PASSWORD = + new SimpleAttributeDefinitionBuilder("truststore-password", ModelType.STRING, true) + .setXmlName("truststore-password") + .setAllowExpression(true) + .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) + .build(); + protected static final SimpleAttributeDefinition CONNECTION_POOL_SIZE = + new SimpleAttributeDefinitionBuilder("connection-pool-size", ModelType.INT, true) + .setXmlName("connection-pool-size") + .setAllowExpression(true) + .setValidator(new IntRangeValidator(0)) + .build(); + + protected static final List REALM_ONLY_ATTRIBUTES = new ArrayList(); + static { + REALM_ONLY_ATTRIBUTES.add(REALM_PUBLIC_KEY); + REALM_ONLY_ATTRIBUTES.add(AUTH_SERVER_URL); + REALM_ONLY_ATTRIBUTES.add(TRUSTSTORE); + REALM_ONLY_ATTRIBUTES.add(TRUSTSTORE_PASSWORD); + REALM_ONLY_ATTRIBUTES.add(SSL_NOT_REQUIRED); + REALM_ONLY_ATTRIBUTES.add(ALLOW_ANY_HOSTNAME); + REALM_ONLY_ATTRIBUTES.add(DISABLE_TRUST_MANAGER); + REALM_ONLY_ATTRIBUTES.add(CONNECTION_POOL_SIZE); + } + + protected static final List ALL_ATTRIBUTES = new ArrayList(); + static { + ALL_ATTRIBUTES.addAll(REALM_ONLY_ATTRIBUTES); + ALL_ATTRIBUTES.addAll(SharedAttributeDefinitons.ATTRIBUTES); + } + + private static final Map DEFINITION_LOOKUP = new HashMap(); + static { + for (SimpleAttributeDefinition def : ALL_ATTRIBUTES) { + DEFINITION_LOOKUP.put(def.getXmlName(), def); + } + } + + private static final RealmWriteAttributeHandler realmAttrHandler = new RealmWriteAttributeHandler(ALL_ATTRIBUTES.toArray(new SimpleAttributeDefinition[0])); + + public RealmDefinition() { + super(PathElement.pathElement("realm"), + KeycloakExtension.getResourceDescriptionResolver("realm"), + RealmAddHandler.INSTANCE, + RealmRemoveHandler.INSTANCE); + } + + @Override + public void registerOperations(ManagementResourceRegistration resourceRegistration) { + super.registerOperations(resourceRegistration); + resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE); + } + + @Override + public void registerAttributes(ManagementResourceRegistration resourceRegistration) { + super.registerAttributes(resourceRegistration); + + for (AttributeDefinition attrDef : ALL_ATTRIBUTES) { + //TODO: use subclass of realmAttrHandler that can call RealmDefinition.validateTruststoreSetIfRequired + resourceRegistration.registerReadWriteAttribute(attrDef, null, realmAttrHandler); + } + } + + /** + * truststore and truststore-password must be set if ssl-not-required and disable-trust-manager are both false. + * + * @param attributes The full set of attributes. + * + * @return true if the attributes are valid, false otherwise. + */ + public static boolean validateTruststoreSetIfRequired(ModelNode attributes) { + if (!isSet(attributes, SSL_NOT_REQUIRED) && !isSet(attributes, DISABLE_TRUST_MANAGER)) { + if (!(isSet(attributes, TRUSTSTORE) && isSet(attributes, TRUSTSTORE_PASSWORD))) { + return false; + } + } + + return true; + } + + private static boolean isSet(ModelNode attributes, SimpleAttributeDefinition def) { + ModelNode attribute = attributes.get(def.getName()); + + if (def.getType() == ModelType.BOOLEAN) { + return attribute.isDefined() && attribute.asBoolean(); + } + + return attribute.isDefined() && !attribute.asString().isEmpty(); + } + + public static SimpleAttributeDefinition lookup(String name) { + return DEFINITION_LOOKUP.get(name); + } +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/RealmRemoveHandler.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/RealmRemoveHandler.java new file mode 100644 index 0000000000..98274875dd --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/RealmRemoveHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import org.jboss.as.controller.AbstractRemoveStepHandler; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.dmr.ModelNode; + +/** + * Remove a realm. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +public class RealmRemoveHandler extends AbstractRemoveStepHandler { + + public static RealmRemoveHandler INSTANCE = new RealmRemoveHandler(); + + private RealmRemoveHandler() {} + + @Override + protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { + KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.find(context); + ckService.removeRealm(operation); + } +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/RealmWriteAttributeHandler.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/RealmWriteAttributeHandler.java new file mode 100644 index 0000000000..f6847b5d5d --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/RealmWriteAttributeHandler.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import java.util.List; +import org.jboss.as.controller.AbstractWriteAttributeHandler; +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.dmr.ModelNode; + +/** + * Update an attribute on a realm. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +public class RealmWriteAttributeHandler extends AbstractWriteAttributeHandler { + + public RealmWriteAttributeHandler(List definitions) { + this(definitions.toArray(new AttributeDefinition[definitions.size()])); + } + + public RealmWriteAttributeHandler(AttributeDefinition... definitions) { + super(definitions); + } + + @Override + protected boolean applyUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName, + ModelNode resolvedValue, ModelNode currentValue, HandbackHolder hh) throws OperationFailedException { + KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.find(context); + ckService.updateRealm(operation, attributeName, resolvedValue); + + hh.setHandback(ckService); + + return false; + } + + @Override + protected void revertUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName, + ModelNode valueToRestore, ModelNode valueToRevert, KeycloakAdapterConfigService ckService) throws OperationFailedException { + ckService.updateRealm(operation, attributeName, valueToRestore); + } + +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/SecureDeploymentAddHandler.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/SecureDeploymentAddHandler.java new file mode 100644 index 0000000000..20930b0abf --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/SecureDeploymentAddHandler.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import java.util.List; +import org.jboss.as.controller.AbstractAddStepHandler; +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.as.controller.ServiceVerificationHandler; +import org.jboss.dmr.ModelNode; + +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP; +import org.jboss.msc.service.ServiceController; + +/** + * Add a deployment to a realm. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +public class SecureDeploymentAddHandler extends AbstractAddStepHandler { + + public static SecureDeploymentAddHandler INSTANCE = new SecureDeploymentAddHandler(); + + private SecureDeploymentAddHandler() {} + + @Override + protected void populateModel(ModelNode operation, ModelNode model) throws OperationFailedException { + // TODO: localize exception. get id number + if (!operation.get(OP).asString().equals(ADD)) { + throw new OperationFailedException("Unexpected operation for add secure deployment. operation=" + operation.toString()); + } + + for (AttributeDefinition attr : SecureDeploymentDefinition.ALL_ATTRIBUTES) { + attr.validateAndSet(operation, model); + } + } + + @Override + protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model, ServiceVerificationHandler verificationHandler, List> newControllers) throws OperationFailedException { + KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.find(context); + ckService.addSecureDeployment(operation, model); + } +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/SecureDeploymentDefinition.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/SecureDeploymentDefinition.java new file mode 100644 index 0000000000..9754c86db9 --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/SecureDeploymentDefinition.java @@ -0,0 +1,108 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.PathElement; +import org.jboss.as.controller.SimpleAttributeDefinition; +import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; +import org.jboss.as.controller.SimpleResourceDefinition; +import org.jboss.as.controller.operations.common.GenericSubsystemDescribeHandler; +import org.jboss.as.controller.operations.validation.StringLengthValidator; +import org.jboss.as.controller.registry.ManagementResourceRegistration; +import org.jboss.dmr.ModelNode; +import org.jboss.dmr.ModelType; + +/** + * Defines attributes and operations for a secure-deployment. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +public class SecureDeploymentDefinition extends SimpleResourceDefinition { + + public static final String TAG_NAME = "secure-deployment"; + + protected static final SimpleAttributeDefinition RESOURCE = + new SimpleAttributeDefinitionBuilder("resource", ModelType.STRING, true) + .setXmlName("resource") + .setAllowExpression(true) + .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) + .build(); + protected static final SimpleAttributeDefinition USE_RESOURCE_ROLE_MAPPINGS = + new SimpleAttributeDefinitionBuilder("use-resource-role-mappings", ModelType.BOOLEAN, true) + .setXmlName("use-resource-role-mappings") + .setAllowExpression(true) + .setDefaultValue(new ModelNode(false)) + .build(); + protected static final SimpleAttributeDefinition BEARER_ONLY = + new SimpleAttributeDefinitionBuilder("bearer-only", ModelType.BOOLEAN, true) + .setXmlName("bearer-only") + .setAllowExpression(true) + .setDefaultValue(new ModelNode(false)) + .build(); + + protected static final List DEPLOYMENT_ONLY_ATTRIBUTES = new ArrayList(); + static { + DEPLOYMENT_ONLY_ATTRIBUTES.add(RESOURCE); + DEPLOYMENT_ONLY_ATTRIBUTES.add(USE_RESOURCE_ROLE_MAPPINGS); + DEPLOYMENT_ONLY_ATTRIBUTES.add(BEARER_ONLY); + } + + protected static final List ALL_ATTRIBUTES = new ArrayList(); + static { + ALL_ATTRIBUTES.addAll(DEPLOYMENT_ONLY_ATTRIBUTES); + ALL_ATTRIBUTES.addAll(SharedAttributeDefinitons.ATTRIBUTES); + } + + private static final Map DEFINITION_LOOKUP = new HashMap(); + static { + for (SimpleAttributeDefinition def : ALL_ATTRIBUTES) { + DEFINITION_LOOKUP.put(def.getXmlName(), def); + } + } + + private static SecureDeploymentWriteAttributeHandler attrHandler = new SecureDeploymentWriteAttributeHandler(ALL_ATTRIBUTES); + + public SecureDeploymentDefinition() { + super(PathElement.pathElement(TAG_NAME), + KeycloakExtension.getResourceDescriptionResolver(TAG_NAME), + SecureDeploymentAddHandler.INSTANCE, + SecureDeploymentRemoveHandler.INSTANCE); + } + + @Override + public void registerOperations(ManagementResourceRegistration resourceRegistration) { + super.registerOperations(resourceRegistration); + resourceRegistration.registerOperationHandler(GenericSubsystemDescribeHandler.DEFINITION, GenericSubsystemDescribeHandler.INSTANCE); + } + + @Override + public void registerAttributes(ManagementResourceRegistration resourceRegistration) { + super.registerAttributes(resourceRegistration); + for (AttributeDefinition attrDef : ALL_ATTRIBUTES) { + resourceRegistration.registerReadWriteAttribute(attrDef, null, attrHandler); + } + } + + public static SimpleAttributeDefinition lookup(String name) { + return DEFINITION_LOOKUP.get(name); + } +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/SecureDeploymentRemoveHandler.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/SecureDeploymentRemoveHandler.java new file mode 100644 index 0000000000..fedd88f5f4 --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/SecureDeploymentRemoveHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import org.jboss.as.controller.AbstractRemoveStepHandler; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.dmr.ModelNode; + +/** + * Remove a secure-deployment from a realm. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +public class SecureDeploymentRemoveHandler extends AbstractRemoveStepHandler { + + public static SecureDeploymentRemoveHandler INSTANCE = new SecureDeploymentRemoveHandler(); + + private SecureDeploymentRemoveHandler() {} + + @Override + protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { + KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.find(context); + ckService.removeSecureDeployment(operation); + } +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/SecureDeploymentWriteAttributeHandler.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/SecureDeploymentWriteAttributeHandler.java new file mode 100644 index 0000000000..8cc1f604d8 --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/SecureDeploymentWriteAttributeHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import java.util.List; +import org.jboss.as.controller.AbstractWriteAttributeHandler; +import org.jboss.as.controller.AttributeDefinition; +import org.jboss.as.controller.OperationContext; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.as.controller.SimpleAttributeDefinition; +import org.jboss.dmr.ModelNode; + +/** + * Update an attribute on a secure-deployment. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +public class SecureDeploymentWriteAttributeHandler extends AbstractWriteAttributeHandler { + + public SecureDeploymentWriteAttributeHandler(List definitions) { + this(definitions.toArray(new AttributeDefinition[definitions.size()])); + } + + public SecureDeploymentWriteAttributeHandler(AttributeDefinition... definitions) { + super(definitions); + } + + @Override + protected boolean applyUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName, + ModelNode resolvedValue, ModelNode currentValue, HandbackHolder hh) throws OperationFailedException { + KeycloakAdapterConfigService ckService = KeycloakAdapterConfigService.find(context); + hh.setHandback(ckService); + ckService.updateSecureDeployment(operation, attributeName, resolvedValue); + return false; + } + + @Override + protected void revertUpdateToRuntime(OperationContext context, ModelNode operation, String attributeName, + ModelNode valueToRestore, ModelNode valueToRevert, KeycloakAdapterConfigService ckService) throws OperationFailedException { + ckService.updateSecureDeployment(operation, attributeName, valueToRestore); + } + +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/extension/SharedAttributeDefinitons.java b/subsystem/src/main/java/org/keycloak/subsystem/extension/SharedAttributeDefinitons.java new file mode 100644 index 0000000000..db0ccf8986 --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/extension/SharedAttributeDefinitons.java @@ -0,0 +1,97 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + +import java.util.ArrayList; +import java.util.List; +import org.jboss.as.controller.SimpleAttributeDefinition; +import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; +import org.jboss.as.controller.operations.validation.IntRangeValidator; +import org.jboss.as.controller.operations.validation.StringLengthValidator; +import org.jboss.dmr.ModelNode; +import org.jboss.dmr.ModelType; + +/** + * Defines attributes that can be present in both a realm and an application (secure-deployment). + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +public class SharedAttributeDefinitons { + + protected static final SimpleAttributeDefinition ENABLE_CORS = + new SimpleAttributeDefinitionBuilder("enable-cors", ModelType.BOOLEAN, true) + .setXmlName("enable-cors") + .setAllowExpression(true) + .setDefaultValue(new ModelNode(false)) + .build(); + protected static final SimpleAttributeDefinition CLIENT_KEYSTORE = + new SimpleAttributeDefinitionBuilder("client-keystore", ModelType.STRING, true) + .setXmlName("client-keystore") + .setAllowExpression(true) + .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) + .build(); + protected static final SimpleAttributeDefinition CLIENT_KEYSTORE_PASSWORD = + new SimpleAttributeDefinitionBuilder("client-keystore-password", ModelType.STRING, true) + .setXmlName("client-keystore-password") + .setAllowExpression(true) + .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) + .build(); + protected static final SimpleAttributeDefinition CLIENT_KEY_PASSWORD = + new SimpleAttributeDefinitionBuilder("client-key-password", ModelType.STRING, true) + .setXmlName("client-key-password") + .setAllowExpression(true) + .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) + .build(); + protected static final SimpleAttributeDefinition CORS_MAX_AGE = + new SimpleAttributeDefinitionBuilder("cors-max-age", ModelType.INT, true) + .setXmlName("cors-max-age") + .setAllowExpression(true) + .setValidator(new IntRangeValidator(-1)) + .build(); + protected static final SimpleAttributeDefinition CORS_ALLOWED_HEADERS = + new SimpleAttributeDefinitionBuilder("cors-allowed-headers", ModelType.STRING, true) + .setXmlName("cors-allowed-headers") + .setAllowExpression(true) + .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) + .build(); + protected static final SimpleAttributeDefinition CORS_ALLOWED_METHODS = + new SimpleAttributeDefinitionBuilder("cors-allowed-methods", ModelType.STRING, true) + .setXmlName("cors-allowed-methods") + .setAllowExpression(true) + .setValidator(new StringLengthValidator(1, Integer.MAX_VALUE, true, true)) + .build(); + protected static final SimpleAttributeDefinition EXPOSE_TOKEN = + new SimpleAttributeDefinitionBuilder("expose-token", ModelType.BOOLEAN, true) + .setXmlName("expose-token") + .setAllowExpression(true) + .setDefaultValue(new ModelNode(false)) + .build(); + + + protected static final List ATTRIBUTES = new ArrayList(); + static { + ATTRIBUTES.add(ENABLE_CORS); + ATTRIBUTES.add(CLIENT_KEYSTORE); + ATTRIBUTES.add(CLIENT_KEYSTORE_PASSWORD); + ATTRIBUTES.add(CLIENT_KEY_PASSWORD); + ATTRIBUTES.add(CORS_MAX_AGE); + ATTRIBUTES.add(CORS_ALLOWED_HEADERS); + ATTRIBUTES.add(CORS_ALLOWED_METHODS); + ATTRIBUTES.add(EXPOSE_TOKEN); + } + +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/logging/KeycloakLogger.java b/subsystem/src/main/java/org/keycloak/subsystem/logging/KeycloakLogger.java new file mode 100644 index 0000000000..bca4588b42 --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/logging/KeycloakLogger.java @@ -0,0 +1,67 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.logging; + +import java.util.List; +import org.jboss.logging.BasicLogger; +import org.jboss.logging.annotations.LogMessage; +import org.jboss.logging.Logger; +import org.jboss.logging.annotations.Message; +import org.jboss.logging.annotations.MessageLogger; +import org.jboss.vfs.VirtualFile; + +import static org.jboss.logging.Logger.Level.ERROR; +import static org.jboss.logging.Logger.Level.INFO; +import static org.jboss.logging.Logger.Level.WARN; + +/** + * This interface to be fleshed out later when error messages are fully externalized. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +@MessageLogger(projectCode = "KEYCLOAK") +public interface KeycloakLogger extends BasicLogger { + + /** + * A logger with a category of the package name. + */ + KeycloakLogger ROOT_LOGGER = Logger.getMessageLogger(KeycloakLogger.class, "org.jboss.keycloak"); + + /* @LogMessage(level = ERROR) + @Message(id = 12600, value = "Could not load JSF managed bean class: %s") + void managedBeanLoadFail(String managedBean); + + @LogMessage(level = ERROR) + @Message(id = 12601, value = "JSF managed bean class %s has no default constructor") + void managedBeanNoDefaultConstructor(String managedBean); + + @LogMessage(level = ERROR) + @Message(id = 12602, value = "Failed to parse %s, managed beans defined in this file will not be available") + void managedBeansConfigParseFailed(VirtualFile facesConfig); + + @LogMessage(level = WARN) + @Message(id = 12603, value = "Unknown JSF version '%s'. Default version '%s' will be used instead.") + void unknownJSFVersion(String version, String defaultVersion); + + @LogMessage(level = WARN) + @Message(id = 12604, value = "JSF version slot '%s' is missing from module %s") + void missingJSFModule(String version, String module); + + @LogMessage(level = INFO) + @Message(id = 12605, value = "Activated the following JSF Implementations: %s") + void activatedJSFImplementations(List target); */ +} diff --git a/subsystem/src/main/java/org/keycloak/subsystem/logging/KeycloakMessages.java b/subsystem/src/main/java/org/keycloak/subsystem/logging/KeycloakMessages.java new file mode 100644 index 0000000000..529049dbfa --- /dev/null +++ b/subsystem/src/main/java/org/keycloak/subsystem/logging/KeycloakMessages.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.logging; + +import org.jboss.as.server.deployment.DeploymentUnitProcessingException; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.DotName; +import org.jboss.logging.annotations.Cause; +import org.jboss.logging.annotations.Message; +import org.jboss.logging.annotations.MessageBundle; +import org.jboss.logging.Messages; + +/** + * This interface to be fleshed out later when error messages are fully externalized. + * + * @author Stan Silvert ssilvert@redhat.com (C) 2012 Red Hat Inc. + */ +@MessageBundle(projectCode = "TLIP") +public interface KeycloakMessages { + + /** + * The messages + */ + KeycloakMessages MESSAGES = Messages.getBundle(KeycloakMessages.class); +/* + @Message(id = 12650, value = "Failed to load annotated class: %s") + String classLoadingFailed(DotName clazz); + + @Message(id = 12651, value = "Annotation %s in class %s is only allowed on classes") + String invalidAnnotationLocation(Object annotation, AnnotationTarget classInfo); + + @Message(id = 12652, value = "Instance creation failed") + RuntimeException instanceCreationFailed(@Cause Throwable t); + + @Message(id = 12653, value = "Instance destruction failed") + RuntimeException instanceDestructionFailed(@Cause Throwable t); + + @Message(id = 12654, value = "Thread local injection container not set") + IllegalStateException noThreadLocalInjectionContainer(); + + @Message(id = 12655, value = "@ManagedBean is only allowed at class level %s") + String invalidManagedBeanAnnotation(AnnotationTarget target); + + @Message(id = 12656, value = "Default JSF implementation slot '%s' is invalid") + DeploymentUnitProcessingException invalidDefaultJSFImpl(String defaultJsfVersion); + */ +} diff --git a/subsystem/src/main/resources/META-INF/services/org.jboss.as.controller.Extension b/subsystem/src/main/resources/META-INF/services/org.jboss.as.controller.Extension new file mode 100644 index 0000000000..6a7d631da0 --- /dev/null +++ b/subsystem/src/main/resources/META-INF/services/org.jboss.as.controller.Extension @@ -0,0 +1 @@ +org.keycloak.subsystem.extension.KeycloakExtension diff --git a/subsystem/src/main/resources/org/keycloak/subsystem/extension/LocalDescriptions.properties b/subsystem/src/main/resources/org/keycloak/subsystem/extension/LocalDescriptions.properties new file mode 100644 index 0000000000..5234a327e3 --- /dev/null +++ b/subsystem/src/main/resources/org/keycloak/subsystem/extension/LocalDescriptions.properties @@ -0,0 +1,49 @@ +keycloak.subsystem=Keycloak subsystem +keycloak.subsystem.add=Operation Adds Keycloak subsystem +keycloak.subsystem.remove=Operation removes Keycloak subsystem +keycloak.subsystem.realm=A Keycloak realm. + +keycloak.realm=A Keycloak realm. +keycloak.realm.add=Add a realm definition to the subsystem. +keycloak.realm.remove=Remove a realm from the subsystem. +keycloak.realm.realm-public-key=TODO: fill in help text +keycloak.realm.auth-server-url=TODO: fill in help text +keycloak.realm.disable-trust-manager=TODO: fill in help text +keycloak.realm.ssl-not-required=TODO: fill in help text +keycloak.realm.allow-any-hostname=TODO: fill in help text +keycloak.realm.truststore=TODO: fill in help text +keycloak.realm.truststore-password=TODO: fill in help text +keycloak.realm.connection-pool-size=TODO: fill in help text +keycloak.realm.enable-cors=TODO: fill in help text +keycloak.realm.client-keystore=TODO: fill in help text +keycloak.realm.client-keystore-password=TODO: fill in help text +keycloak.realm.client-key-password=TODO: fill in help text +keycloak.realm.cors-max-age=TODO: fill in help text +keycloak.realm.cors-allowed-headers=TODO: fill in help text +keycloak.realm.cors-allowed-methods=TODO: fill in help text +keycloak.realm.expose-token=TODO: fill in help text + +keycloak.realm.secure-deployment=A deployment secured by Keycloak + +keycloak.secure-deployment=A deployment secured by Keycloak +keycloak.secure-deployment.add=Add a deployment to be secured by Keycloak +keycloak.secure-deployment.remove=Remove a deployment to be secured by Keycloak +keycloak.secure-deployment.resource=TODO: fill in help text +keycloak.secure-deployment.use-resource-role-mappings=TODO: fill in help text +keycloak.secure-deployment.credentials=TODO: fill in help text +keycloak.secure-deployment.bearer-only=TODO: fill in help text +keycloak.secure-deployment.enable-cors=TODO: fill in help text +keycloak.secure-deployment.client-keystore=TODO: fill in help text +keycloak.secure-deployment.client-keystore-password=TODO: fill in help text +keycloak.secure-deployment.client-key-password=TODO: fill in help text +keycloak.secure-deployment.cors-max-age=TODO: fill in help text +keycloak.secure-deployment.cors-allowed-headers=TODO: fill in help text +keycloak.secure-deployment.cors-allowed-methods=TODO: fill in help text +keycloak.secure-deployment.expose-token=TODO: fill in help text + +keycloak.secure-deployment.credential=TODO: fill in help text + +keycloak.credential=TODO: fill in help text +keycloak.credential.value=TODO: fill in help text +keycloak.credential.add=TODO: fill in help text +keycloak.credential.remove=TODO: fill in help text \ No newline at end of file diff --git a/subsystem/src/main/resources/schema/keycloak_1_0.xsd b/subsystem/src/main/resources/schema/keycloak_1_0.xsd new file mode 100644 index 0000000000..9cdbbba980 --- /dev/null +++ b/subsystem/src/main/resources/schema/keycloak_1_0.xsd @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + The name of the realm. + + + + + + + + + + + + + + + + + + + + + + + + The name of the deployment. + + + + + + + + + + + + + + + + The name of the credential. + + + + + + + + + + + + + + + + + + + + + diff --git a/subsystem/src/test/java/org/keycloak/subsystem/extension/RealmDefinitionTestCase.java b/subsystem/src/test/java/org/keycloak/subsystem/extension/RealmDefinitionTestCase.java new file mode 100644 index 0000000000..5cff75e2c5 --- /dev/null +++ b/subsystem/src/test/java/org/keycloak/subsystem/extension/RealmDefinitionTestCase.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + + +import org.jboss.dmr.ModelNode; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + + + +/** + * + * @author Stan Silvert ssilvert@redhat.com (C) 2013 Red Hat Inc. + */ +public class RealmDefinitionTestCase { + + private ModelNode model; + + @Before + public void setUp() { + model = new ModelNode(); + model.get("realm").set("demo"); + model.get("resource").set("customer-portal"); + model.get("realm-public-key").set("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"); + model.get("auth-url").set("http://localhost:8080/auth-server/rest/realms/demo/tokens/login"); + model.get("code-url").set("http://localhost:8080/auth-server/rest/realms/demo/tokens/access/codes"); + model.get("expose-token").set(true); + ModelNode credential = new ModelNode(); + credential.get("password").set("password"); + model.get("credentials").set(credential); + } + + @Test + public void testIsTruststoreSetIfRequired() throws Exception { + model.get("ssl-not-required").set(true); + model.get("disable-trust-manager").set(true); + Assert.assertTrue(RealmDefinition.validateTruststoreSetIfRequired(model)); + + model.get("ssl-not-required").set(true); + model.get("disable-trust-manager").set(false); + Assert.assertTrue(RealmDefinition.validateTruststoreSetIfRequired(model)); + + model.get("ssl-not-required").set(false); + model.get("disable-trust-manager").set(true); + Assert.assertTrue(RealmDefinition.validateTruststoreSetIfRequired(model)); + + model.get("ssl-not-required").set(false); + model.get("disable-trust-manager").set(false); + Assert.assertFalse(RealmDefinition.validateTruststoreSetIfRequired(model)); + + model.get("ssl-not-required").set(false); + model.get("disable-trust-manager").set(false); + model.get("truststore").set("foo"); + Assert.assertFalse(RealmDefinition.validateTruststoreSetIfRequired(model)); + + model.get("ssl-not-required").set(false); + model.get("disable-trust-manager").set(false); + model.get("truststore").set("foo"); + model.get("truststore-password").set("password"); + Assert.assertTrue(RealmDefinition.validateTruststoreSetIfRequired(model)); + } + +} diff --git a/subsystem/src/test/java/org/keycloak/subsystem/extension/SubsystemParsingTestCase.java b/subsystem/src/test/java/org/keycloak/subsystem/extension/SubsystemParsingTestCase.java new file mode 100644 index 0000000000..090feb12e8 --- /dev/null +++ b/subsystem/src/test/java/org/keycloak/subsystem/extension/SubsystemParsingTestCase.java @@ -0,0 +1,175 @@ +/* + * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors + * as indicated by the @author tags. All rights reserved. + * + * 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.extension; + + +import junit.framework.Assert; +import org.jboss.as.controller.PathAddress; +import org.jboss.as.controller.PathElement; +import org.jboss.as.subsystem.test.AbstractSubsystemTest; +import org.jboss.as.subsystem.test.KernelServices; +import org.jboss.dmr.ModelNode; +import org.junit.Test; + +import java.util.List; + +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.DESCRIBE; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR; +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM; + + +/** + * Tests all management expects for subsystem, parsing, marshaling, model definition and other + * Here is an example that allows you a fine grained controller over what is tested and how. So it can give you ideas what can be done and tested. + * If you have no need for advanced testing of subsystem you look at {@link SubsystemBaseParsingTestCase} that testes same stuff but most of the code + * is hidden inside of test harness + * + * @author Kabir Khan + */ +public class SubsystemParsingTestCase extends AbstractSubsystemTest { + + public SubsystemParsingTestCase() { + super(KeycloakExtension.SUBSYSTEM_NAME, new KeycloakExtension()); + } + + @Test + public void testJson() throws Exception { + ModelNode node = new ModelNode(); + node.get("realm").set("demo"); + node.get("resource").set("customer-portal"); + node.get("realm-public-key").set("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"); + node.get("auth-url").set("http://localhost:8080/auth-server/rest/realms/demo/tokens/login"); + node.get("code-url").set("http://localhost:8080/auth-server/rest/realms/demo/tokens/access/codes"); + node.get("ssl-not-required").set(true); + node.get("expose-token").set(true); + ModelNode credential = new ModelNode(); + credential.get("password").set("password"); + node.get("credentials").set(credential); + + System.out.println("json=" + node.toJSONString(false)); + } + /** + * Tests that the xml is parsed into the correct operations + */ + @Test + public void testParseSubsystem() throws Exception { + //Parse the subsystem xml into operations + String subsystemXml = + "" + + ""; + List operations = super.parse(subsystemXml); + + ///Check that we have the expected number of operations + Assert.assertEquals(1, operations.size()); + + //Check that each operation has the correct content + ModelNode addSubsystem = operations.get(0); + Assert.assertEquals(ADD, addSubsystem.get(OP).asString()); + PathAddress addr = PathAddress.pathAddress(addSubsystem.get(OP_ADDR)); + Assert.assertEquals(1, addr.size()); + PathElement element = addr.getElement(0); + Assert.assertEquals(SUBSYSTEM, element.getKey()); + Assert.assertEquals(KeycloakExtension.SUBSYSTEM_NAME, element.getValue()); + } + + /** + * Test that the model created from the xml looks as expected + */ + @Test + public void testInstallIntoController() throws Exception { + //Parse the subsystem xml and install into the controller + String subsystemXml = + "" + + ""; + KernelServices services = super.installInController(subsystemXml); + + //Read the whole model and make sure it looks as expected + ModelNode model = services.readWholeModel(); + Assert.assertTrue(model.get(SUBSYSTEM).hasDefined(KeycloakExtension.SUBSYSTEM_NAME)); + } + + /** + * Starts a controller with a given subsystem xml and then checks that a second + * controller started with the xml marshalled from the first one results in the same model + */ + @Test + public void testParseAndMarshalModel() throws Exception { + //Parse the subsystem xml and install into the first controller + //TODO: Figure out why this fails + String subsystemXml = + "" + + ""; + KernelServices servicesA = super.installInController(subsystemXml); + //Get the model and the persisted xml from the first controller + ModelNode modelA = servicesA.readWholeModel(); + String marshalled = servicesA.getPersistedSubsystemXml(); + + //Install the persisted xml from the first controller into a second controller + KernelServices servicesB = super.installInController(marshalled); + ModelNode modelB = servicesB.readWholeModel(); + + //Make sure the models from the two controllers are identical + super.compare(modelA, modelB); + } + + /** + * Starts a controller with the given subsystem xml and then checks that a second + * controller started with the operations from its describe action results in the same model + */ + @Test + public void testDescribeHandler() throws Exception { + //Parse the subsystem xml and install into the first controller + String subsystemXml = + "" + + ""; + KernelServices servicesA = super.installInController(subsystemXml); + //Get the model and the describe operations from the first controller + ModelNode modelA = servicesA.readWholeModel(); + ModelNode describeOp = new ModelNode(); + describeOp.get(OP).set(DESCRIBE); + describeOp.get(OP_ADDR).set( + PathAddress.pathAddress( + PathElement.pathElement(SUBSYSTEM, KeycloakExtension.SUBSYSTEM_NAME)).toModelNode()); + List operations = super.checkResultAndGetContents(servicesA.executeOperation(describeOp)).asList(); + + + //Install the describe options from the first controller into a second controller + KernelServices servicesB = super.installInController(operations); + ModelNode modelB = servicesB.readWholeModel(); + + //Make sure the models from the two controllers are identical + super.compare(modelA, modelB); + } + + /** + * Tests that the subsystem can be removed + */ + @Test + public void testSubsystemRemoval() throws Exception { + //Parse the subsystem xml and install into the first controller + String subsystemXml = + "" + + ""; + KernelServices services = super.installInController(subsystemXml); + //Checks that the subsystem was removed from the model + super.assertRemoveSubsystemResources(services); + + //TODO Chek that any services that were installed were removed here + } +}