/* * JBoss, Home of Professional Open Source. * Copyright 2010, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.controller.operations.global; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ACCESS_CONTROL; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADDRESS; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.CHILD_TYPE; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FAILURE_DESCRIPTION; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.HOST; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.INCLUDE_RUNTIME; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.NAME; 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.OUTCOME; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_ATTRIBUTE_OPERATION; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_CHILDREN_NAMES_OPERATION; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_CHILDREN_RESOURCES_OPERATION; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_RESOURCE_DESCRIPTION_OPERATION; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_RESOURCE_OPERATION; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESPONSE_HEADERS; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESULT; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RUNNING_SERVER; import static org.jboss.as.controller.operations.global.GlobalOperationAttributes.RECURSIVE; import static org.jboss.as.controller.operations.global.GlobalOperationAttributes.RECURSIVE_DEPTH; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; import org.jboss.as.controller.ModelVersion; import org.jboss.as.controller.OperationContext; import org.jboss.as.controller.OperationFailedException; import org.jboss.as.controller.OperationStepHandler; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.PathElement; import org.jboss.as.controller.ProcessType; import org.jboss.as.controller.UnauthorizedException; import org.jboss.as.controller._private.OperationFailedRuntimeException; import org.jboss.as.controller.access.Action; import org.jboss.as.controller.access.AuthorizationResult; import org.jboss.as.controller.access.ResourceNotAddressableException; import org.jboss.as.controller.access.rbac.UnknowRoleException; import org.jboss.as.controller.logging.ControllerLogger; import org.jboss.as.controller.operations.common.Util; import org.jboss.as.controller.registry.AliasEntry; import org.jboss.as.controller.registry.AliasEntry.AliasContext; import org.jboss.as.controller.registry.ImmutableManagementResourceRegistration; import org.jboss.as.controller.registry.ManagementResourceRegistration; import org.jboss.as.controller.registry.Resource; import org.jboss.as.controller.registry.WildcardReadResourceDescriptionAddressHack; import org.jboss.dmr.ModelNode; import org.jboss.dmr.ModelType; /** * Global {@code OperationHandler}s. * * @author <a href="kabir.khan@jboss.com">Kabir Khan</a> */ public class GlobalOperationHandlers { public static void registerGlobalOperations(ManagementResourceRegistration root, ProcessType processType) { if( processType == ProcessType.HOST_CONTROLLER) { root.registerOperationHandler(org.jboss.as.controller.operations.global.ReadResourceHandler.DEFINITION, org.jboss.as.controller.operations.global.ReadResourceHandler.INSTANCE, true); root.registerOperationHandler(org.jboss.as.controller.operations.global.ReadAttributeHandler.DEFINITION, org.jboss.as.controller.operations.global.ReadAttributeHandler.INSTANCE, true); root.registerOperationHandler(ReadAttributeGroupHandler.DEFINITION, ReadAttributeGroupHandler.INSTANCE, true); }else{ root.registerOperationHandler(org.jboss.as.controller.operations.global.ReadResourceHandler.RESOLVE_DEFINITION, org.jboss.as.controller.operations.global.ReadResourceHandler.RESOLVE_INSTANCE, true); root.registerOperationHandler(org.jboss.as.controller.operations.global.ReadAttributeHandler.RESOLVE_DEFINITION, org.jboss.as.controller.operations.global.ReadAttributeHandler.RESOLVE_INSTANCE, true); root.registerOperationHandler(ReadAttributeGroupHandler.RESOLVE_DEFINITION, ReadAttributeGroupHandler.RESOLVE_INSTANCE, true); } root.registerOperationHandler(ReadResourceDescriptionHandler.DEFINITION, ReadResourceDescriptionHandler.INSTANCE, true); root.registerOperationHandler(ReadAttributeGroupNamesHandler.DEFINITION, ReadAttributeGroupNamesHandler.INSTANCE, true); root.registerOperationHandler(ReadChildrenNamesHandler.DEFINITION, ReadChildrenNamesHandler.INSTANCE, true); root.registerOperationHandler(ReadChildrenTypesHandler.DEFINITION, ReadChildrenTypesHandler.INSTANCE, true); root.registerOperationHandler(ReadChildrenResourcesHandler.DEFINITION, ReadChildrenResourcesHandler.INSTANCE, true); root.registerOperationHandler(ReadOperationNamesHandler.DEFINITION, ReadOperationNamesHandler.INSTANCE, true); root.registerOperationHandler(ReadOperationDescriptionHandler.DEFINITION, ReadOperationDescriptionHandler.INSTANCE, true); root.registerOperationHandler(QueryOperationHandler.DEFINITION, QueryOperationHandler.INSTANCE, true); //map operations root.registerOperationHandler(MapOperations.MAP_PUT_DEFINITION, MapOperations.MAP_PUT_HANDLER, true); root.registerOperationHandler(MapOperations.MAP_GET_DEFINITION, MapOperations.MAP_GET_HANDLER, true); root.registerOperationHandler(MapOperations.MAP_REMOVE_DEFINITION, MapOperations.MAP_REMOVE_HANDLER, true); root.registerOperationHandler(MapOperations.MAP_CLEAR_DEFINITION, MapOperations.MAP_CLEAR_HANDLER, true); //list operations root.registerOperationHandler(ListOperations.LIST_ADD_DEFINITION, ListOperations.LIST_ADD_HANDLER, true); root.registerOperationHandler(ListOperations.LIST_REMOVE_DEFINITION, ListOperations.LIST_REMOVE_HANDLER, true); root.registerOperationHandler(ListOperations.LIST_GET_DEFINITION, ListOperations.LIST_GET_HANDLER, true); root.registerOperationHandler(ListOperations.LIST_CLEAR_DEFINITION, ListOperations.LIST_CLEAR_HANDLER, true); root.registerOperationHandler(ReadResourceDescriptionHandler.CheckResourceAccessHandler.DEFINITION, new OperationStepHandler() { @Override public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { //Just use an empty operation handler here, this is a private operation and people who want to call it need to instantiate the step handler throw new OperationFailedException("This should never be called"); } }, true); root.registerOperationHandler(ReadResourceDescriptionHandler.CheckResourceAccessHandler.DEFAULT_DEFINITION, new OperationStepHandler() { @Override public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { //Just use an empty operation handler here, this is a private operation and people who want to call it need to instantiate the step handler throw new OperationFailedException("This should never be called"); } }, true); root.registerOperationHandler(org.jboss.as.controller.operations.global.WriteAttributeHandler.DEFINITION, org.jboss.as.controller.operations.global.WriteAttributeHandler.INSTANCE, true); root.registerOperationHandler(org.jboss.as.controller.operations.global.UndefineAttributeHandler.DEFINITION, org.jboss.as.controller.operations.global.UndefineAttributeHandler.INSTANCE, true); } public static final String CHECK_DEFAULT_RESOURCE_ACCESS = "check-default-resource-access"; public static final String CHECK_RESOURCE_ACCESS = "check-resource-access"; private GlobalOperationHandlers() { // } public abstract static class AbstractMultiTargetHandler implements OperationStepHandler { public static final ModelNode FAKE_OPERATION; static { final ModelNode resolve = new ModelNode(); resolve.get(OP).set("resolve"); resolve.get(OP_ADDR).setEmptyList(); resolve.protect(); FAKE_OPERATION = resolve; } private final FilteredData filteredData; private final boolean ignoreMissingResource; private final FilterPredicate predicate; private final boolean registryOnly; protected AbstractMultiTargetHandler() { this(null, false); } protected AbstractMultiTargetHandler(boolean registryOnly) { this(null, false, null, registryOnly); } protected AbstractMultiTargetHandler(FilteredData filteredData) { this(filteredData, false); } protected AbstractMultiTargetHandler(FilteredData filteredData, boolean ignoreMissingResource) { this(filteredData, ignoreMissingResource, null); } protected AbstractMultiTargetHandler(FilteredData filteredData, boolean ignoreMissingResource, FilterPredicate predicate) { this(filteredData, ignoreMissingResource, predicate, false); } private AbstractMultiTargetHandler(FilteredData filteredData, boolean ignoreMissingResource, FilterPredicate predicate, boolean registryOnly) { this.filteredData = filteredData; this.ignoreMissingResource = ignoreMissingResource; this.predicate = predicate; this.registryOnly = registryOnly; } protected FilteredData getFilteredData() { return filteredData; } @Override public void execute(final OperationContext context, final ModelNode operation) throws OperationFailedException { final PathAddress address = context.getCurrentAddress(); // In case if it's a multiTarget operation, resolve the address first // This only works for model resources, which can be resolved into a concrete addresses if (address.isMultiTarget()) { final FilteredData localFilteredData = filteredData == null ? new FilteredData(PathAddress.EMPTY_ADDRESS) : filteredData; // The final result should be a list of executed operations final ModelNode result = context.getResult().setEmptyList(); // Trick the context to give us the model-root final OperationStepHandler delegateStepHandler = new OperationStepHandler() { @Override public void execute(final OperationContext context, final ModelNode operation) throws OperationFailedException { doExecute(context, operation, localFilteredData, true); } }; final ModelNode fakeOperationResponse = new ModelNode(); context.addStep(fakeOperationResponse, FAKE_OPERATION.clone(), registryOnly ? new RegistrationAddressResolver(operation, result, delegateStepHandler) : new ModelAddressResolver(operation, result, localFilteredData, delegateStepHandler, predicate), OperationContext.Stage.MODEL, true ); context.completeStep(new MultiTargetResultHandler(fakeOperationResponse, localFilteredData, result)); } else { doExecute(context, operation, filteredData, ignoreMissingResource); } } /** * Execute the actual operation if it is not addressed to multiple targets. * * * @param context the operation context * @param operation the original operation * @param filteredData tracking object for filtered data * @param ignoreMissingResource {@code false} if execution should throw * {@link org.jboss.as.controller.registry.Resource.NoSuchResourceException} if the * targeted resource does not exist; {@code true} if it should simply * not provide a result * @throws OperationFailedException */ abstract void doExecute(OperationContext context, ModelNode operation, FilteredData filteredData, boolean ignoreMissingResource) throws OperationFailedException; private static class MultiTargetResultHandler implements OperationContext.ResultHandler { private final FilteredData localFilteredData; private final ModelNode result; private final ModelNode fakeOperationResponse; public MultiTargetResultHandler(ModelNode fakeOperationResponse, FilteredData localFilteredData, ModelNode result) { this.localFilteredData = localFilteredData; this.result = result; this.fakeOperationResponse = fakeOperationResponse; } @Override public void handleResult(OperationContext.ResultAction resultAction, OperationContext context, ModelNode operation) { if(fakeOperationResponse != null && fakeOperationResponse.hasDefined(FAILURE_DESCRIPTION)) { context.getFailureDescription().set(fakeOperationResponse.get(FAILURE_DESCRIPTION)); return; } // Report on filtering if (localFilteredData.hasFilteredData()) { context.getResponseHeaders().get(ACCESS_CONTROL).set(localFilteredData.toModelNode()); } // Extract any failure info from the individual results and use them // to construct an overall failure description if necessary if (resultAction == OperationContext.ResultAction.ROLLBACK && !context.hasFailureDescription() && result.isDefined()) { String op = operation.require(OP).asString(); Map<PathAddress, ModelNode> failures = new HashMap<PathAddress, ModelNode>(); for (ModelNode resultItem : result.asList()) { if (resultItem.hasDefined(FAILURE_DESCRIPTION)) { final PathAddress failedAddress = PathAddress.pathAddress(resultItem.get(ADDRESS)); ModelNode failedDesc = resultItem.get(FAILURE_DESCRIPTION); failures.put(failedAddress, failedDesc); } } if (failures.size() == 1) { Map.Entry<PathAddress, ModelNode> entry = failures.entrySet().iterator().next(); if (entry.getValue().getType() == ModelType.STRING) { context.getFailureDescription().set(ControllerLogger.ROOT_LOGGER.wildcardOperationFailedAtSingleAddress(op, entry.getKey(), entry.getValue().asString())); } else { context.getFailureDescription().set(ControllerLogger.ROOT_LOGGER.wildcardOperationFailedAtSingleAddressWithComplexFailure(op, entry.getKey())); } } else if (failures.size() > 1) { context.getFailureDescription().set(ControllerLogger.ROOT_LOGGER.wildcardOperationFailedAtMultipleAddresses(op, failures.keySet())); } } } } } @FunctionalInterface interface FilterPredicate extends Predicate<ModelNode> { } private abstract static class AbstractAddressResolver implements OperationStepHandler { private static final FilterPredicate DEFAULT_PREDICATE = item -> !item.isDefined() || !item.hasDefined(OP_ADDR); private final ModelNode operation; private final ModelNode result; private final FilteredData filteredData; private final FilterPredicate predicate; private final OperationStepHandler handler; // handler bypassing further wildcard resolution public AbstractAddressResolver(final ModelNode operation, final ModelNode result, final OperationStepHandler delegate, final FilteredData filteredData, final FilterPredicate predicate) { this.operation = operation; this.result = result; this.handler = delegate; this.predicate = predicate == null ? DEFAULT_PREDICATE : predicate; this.filteredData = filteredData; } /** * {@inheritDoc} */ @Override public void execute(final OperationContext context, final ModelNode ignored) throws OperationFailedException { final PathAddress addr = PathAddress.pathAddress(operation.require(OP_ADDR)); //This will only return the alias if this is for a read-resource-definition final PathAddress aliasAddr = WildcardReadResourceDescriptionAddressHack.detachAliasAddress(context, operation); final PathAddress address = aliasAddr == null ? addr : aliasAddr; execute(PathAddress.EMPTY_ADDRESS, address, context, context.getRootResourceRegistration(), true); context.completeStep(new OperationContext.ResultHandler() { @Override public void handleResult(OperationContext.ResultAction resultAction, OperationContext context, ModelNode operation) { if (result.getType() == ModelType.LIST) { boolean replace = false; ModelNode replacement = new ModelNode().setEmptyList(); for (ModelNode item : result.asList()) { if (predicate.test(item)) { // item will be skipped and the result amended replace = true; } else { replacement.add(item); } } if (replace) { result.set(replacement); } } } }); } /** * Wraps a call to * {@link #execute(org.jboss.as.controller.PathAddress, org.jboss.as.controller.PathAddress, org.jboss.as.controller.OperationContext, org.jboss.as.controller.registry.ImmutableManagementResourceRegistration, boolean)} * with logic to handle authorization and missing resource exceptions. * * @param base the portion of the address that has already been processed * @param remaining the unprocessed portion of the original address * @param context context of the request * @param registration the resource registration for the {@code base} address * @param ignoreMissing {@code true} if this is being called for an "optional" portion of an address tree * and NoSuchResourceException should be ignored. A portion of an address * tree is optional if it is associated with a wildcard or multi-segment * element in {@code address} */ protected void safeExecute(final PathAddress base, final PathAddress remaining, final OperationContext context, final ImmutableManagementResourceRegistration registration, boolean ignoreMissing) { try { ControllerLogger.MGMT_OP_LOGGER.tracef("safeExecute for %s, remaining is %s", base, remaining); execute(base, remaining, context, registration, ignoreMissing); } catch (UnauthorizedException e) { // equivalent to the resource not existing // Just report the failure to the filter and complete normally filteredData.addReadRestrictedResource(base); ControllerLogger.MGMT_OP_LOGGER.tracef("Caught UnauthorizedException in %s", this); } catch (ResourceNotAddressableException e) { // Just report the failure to the filter and complete normally filteredData.addAccessRestrictedResource(base); ControllerLogger.MGMT_OP_LOGGER.tracef("Caught ResourceNotAddressableException in %s", this); } catch (Resource.NoSuchResourceException e) { // It's possible this is a remote failure, in which case we // don't get ResourceNotAddressableException. So see if // it was due to any authorization denial ModelNode toAuthorize = Util.createEmptyOperation(READ_RESOURCE_OPERATION, base); AuthorizationResult.Decision decision = context.authorize(toAuthorize, EnumSet.of(Action.ActionEffect.ADDRESS)).getDecision(); ControllerLogger.MGMT_OP_LOGGER.tracef("Caught NoSuchResourceException in %s. Authorization decision is %s", this, decision); if (decision == AuthorizationResult.Decision.DENY) { // Just report the failure to the filter and complete normally filteredData.addAccessRestrictedResource(base); } else if (!ignoreMissing) { throw e; } } } /** * Resolve matching addresses for the portions of {@code address} equal to or below {@code base}, * adding a step to invoke the {@code this.handler} for each real address that is a leaf. Wildcards * and multi-segment addresses are handled. * * @param base the portion of the address that has already been processed * @param remaining the unprocessed portion of the original address * @param context context of the request * @param registration the resource registration for the {@code base} address * @param ignoreMissing {@code true} if this is being called for an "optional" portion of an address tree * and NoSuchResourceException should be ignored. A portion of an address * tree is optional if it is associated with a wildcard or multi-segment * element in {@code address} */ protected void execute(final PathAddress base, final PathAddress remaining, final OperationContext context, final ImmutableManagementResourceRegistration registration, final boolean ignoreMissing) { // Check whether the operation needs to be dispatched to a remote proxy if (registration.isRemote()) { if (isWFCORE621Needed(registration, remaining)) { executeWFCORE621(base, remaining, context, registration, ignoreMissing); } else { executeRemote(base, remaining, context, registration, ignoreMissing); } // No further processing needed return; } if (!authorize(context, base, operation)) { return; } if (remaining.size() > 0) { final PathElement currentElement = remaining.getElement(0); final PathAddress newRemaining = remaining.subAddress(1); if (currentElement.isMultiTarget()) { executeMultiTargetChildren(base, currentElement, newRemaining, context, registration, ignoreMissing); } else { executeSingleTargetChild(base, currentElement, newRemaining, context, ignoreMissing); } } else { final ModelNode newOp = operation.clone(); newOp.get(OP_ADDR).set(base.toModelNode()); final ModelNode resultItem = this.result.add(); ControllerLogger.MGMT_OP_LOGGER.tracef("Added ModelAddressResolver result item for %s", base); final ModelNode resultAddress = resultItem.get(OP_ADDR); final OperationStepHandler wrapper = new OperationStepHandler() { @Override public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { try { handler.execute(context, operation); context.completeStep(new OperationContext.ResultHandler() { @Override public void handleResult(OperationContext.ResultAction resultAction, OperationContext context, ModelNode operation) { ControllerLogger.MGMT_OP_LOGGER.tracef("ModelAddressResolver result for %s is %s", base, resultItem); if (resultItem.hasDefined(RESULT)) { resultAddress.set(base.toModelNode()); if (resultItem.hasDefined(RESPONSE_HEADERS, ACCESS_CONTROL)) { ModelNode headers = resultItem.get(RESPONSE_HEADERS); ModelNode acc = headers.remove(ACCESS_CONTROL); if (headers.asInt() == 0) { resultItem.remove(RESPONSE_HEADERS); } filteredData.populate(acc, PathAddress.EMPTY_ADDRESS); } } else { resultItem.clear(); } } }); } catch (Resource.NoSuchResourceException e) { // just discard the result to avoid leaking the inaccessible address } } }; context.addStep(resultItem, newOp, wrapper, OperationContext.Stage.MODEL, true); } } protected abstract void executeSingleTargetChild(PathAddress base, PathElement currentElement, PathAddress newRemaining, OperationContext context, boolean ignoreMissing); protected abstract void executeMultiTargetChildren(PathAddress base, PathElement currentElement, PathAddress newRemaining, OperationContext context, ImmutableManagementResourceRegistration registration, boolean ignoreMissing); /** * If not authorized, this will throw an exception for {@link ModelAddressResolver} for use with the * {@link ModelAddressResolver#safeExecute(PathAddress, PathAddress, OperationContext, ImmutableManagementResourceRegistration, boolean)} * method. For {@link RegistrationAddressResolver} it will return {@code false}. Otherwise it returns {@code true} * * @param context the operation context * @param base the path address * @param operation the operation * @return whether or not we were authorized */ protected abstract boolean authorize(OperationContext context, PathAddress base, ModelNode operation); private boolean isWFCORE621Needed(ImmutableManagementResourceRegistration registration, PathAddress remaining) { if (remaining.size() > 0) { PathElement pe = remaining.getElement(0); if (pe.isMultiTarget() && RUNNING_SERVER.equals(pe.getKey())) { // We only need this for WildFly 8 and earlier (including EAP 6), // so that's proxied controllers running kernel version 1.x or 2.x ModelVersion modelVersion = registration.getProxyController(PathAddress.EMPTY_ADDRESS).getKernelModelVersion(); return modelVersion.getMajor() < 3; } } return false; } private void executeWFCORE621(PathAddress base, PathAddress remaining, OperationContext context, ImmutableManagementResourceRegistration registration, boolean ignoreMissing) { ControllerLogger.MGMT_OP_LOGGER.tracef("Executing WFCORE-621 op for base %s and remaining %s", base, remaining); // We have distinct handling for WildFly 8 // TODO a mixed domain of WildFly > 9 managing WildFly 8 is unlikely to work, so this can likely be dropped final boolean wildfly8 = registration.getProxyController(PathAddress.EMPTY_ADDRESS).getKernelModelVersion().getMajor() == 2; // We have a request for /host=foo/server=*[/...] targeted at a host that // doesn't have the WFCORE-282 fix available and thus can't handle that request. // So, we are going to execute a step to have it provide us the names of all // its servers, and then a step that will loop through the server names and // add the usual execution for each final ModelNode serverNameResponse = new ModelNode(); final AtomicBoolean filtered = new AtomicBoolean(false); // We're adding steps to the top of the queue, so add the one that will use the server names first context.addStep(new OperationStepHandler() { @Override public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { ControllerLogger.MGMT_OP_LOGGER.tracef("Executing WFCORE-621 2nd step for base %s and remaining %s; filtered? %s serverNames=%s", base, remaining, filtered, serverNameResponse); // If the read of server names was filtered or for some other reason we didn't get them, we are done. if (filtered.get() || !serverNameResponse.hasDefined(RESULT)) { return; } Set<String> targetServers = extractServerNames(serverNameResponse.get(RESULT), operation, remaining, wildfly8); PathAddress afterServer = remaining.size() > 1 ? remaining.subAddress(1) : PathAddress.EMPTY_ADDRESS; for (String targetServer : targetServers) { PathAddress newBase = base.append(PathElement.pathElement(RUNNING_SERVER, targetServer)); safeExecute(newBase, afterServer, context, registration, ignoreMissing); } } }, OperationContext.Stage.MODEL, true); // Now add the step to read the server names. // For WildFly 8 slaves we use read-children-resources because read-children-names includes // server names that have a server-config but aren't started. So in the handler above // we use the resource node to distinguish those cases final String opName = wildfly8 ? READ_CHILDREN_RESOURCES_OPERATION : READ_CHILDREN_NAMES_OPERATION; ModelNode op = Util.createEmptyOperation(opName, base); op.get(CHILD_TYPE).set(RUNNING_SERVER); OperationStepHandler proxyHandler = registration.getOperationHandler(PathAddress.EMPTY_ADDRESS, opName); // Use a custom handler to deal with the remote host not being readable (e.g. RBAC) OperationStepHandler filterableHandler = new FilterableRemoteOperationStepHandler(proxyHandler, base, filtered, filteredData, ignoreMissing); context.addStep(serverNameResponse, op, filterableHandler, OperationContext.Stage.MODEL, true); } private static Set<String> extractServerNames(ModelNode serverResultNode, ModelNode operation, PathAddress remaining, boolean wildfly8) { PathElement serverPE = remaining.getElement(0); Set<String> interestingServers = null; if (!serverPE.isWildcard()) { interestingServers = new HashSet<String>(); Collections.addAll(interestingServers, serverPE.getSegments()); } Set<String> result = new LinkedHashSet<>(); if (wildfly8) { // TODO a mixed domain of WildFly > 9 managing WildFly 8 is unlikely to work, so all this can likely be dropped // The op we ran was read-children-resources, so we got back an object for (String serverName : serverResultNode.keys()) { if (interestingServers == null || interestingServers.contains(serverName)) { ModelNode serverVal = serverResultNode.get(serverName); // If we get an undefined or empty node this indicates there's just a placeholder resource // for a non-started server-config boolean validServer = serverVal.isDefined() && serverVal.asInt() > 0; if (!validServer && remaining.size() == 1) { // Request was for the server node itself. // Begin horrendous hacks for WildFly 8 support, where a runtime-only resource // with a couple of attributes is available for non-started servers. String opName = operation.get(OP).asString(); if (READ_ATTRIBUTE_OPERATION.equals(opName)) { String attrName = operation.get(NAME).asString(); validServer = "launch-type".equals(attrName) || "server-state".equals(attrName); } else if (READ_RESOURCE_OPERATION.equals(opName)) { validServer = operation.hasDefined(INCLUDE_RUNTIME) && operation.get(INCLUDE_RUNTIME).asBoolean(); } } if (validServer) { result.add(serverName); } } } } else { // EAP 6 case // The op we ran was read-children-names so we got back a list of string for (ModelNode serverNameNode : serverResultNode.asList()) { String serverName = serverNameNode.asString(); if (interestingServers == null || interestingServers.contains(serverName)) { result.add(serverName); } } } return result; } private void executeRemote(final PathAddress base, final PathAddress remaining, OperationContext context, ImmutableManagementResourceRegistration registration, final boolean ignoreMissing) { // make sure the target address does not contain the unresolved elements of the address final ModelNode remoteOp = operation.clone(); final PathAddress fullAddress = base.append(remaining); remoteOp.get(OP_ADDR).set(fullAddress.toModelNode()); // Temp remote result final ModelNode resultItem = new ModelNode(); final OperationStepHandler proxyHandler = registration.getOperationHandler(PathAddress.EMPTY_ADDRESS, operation.require(OP).asString()); context.addStep(resultItem, remoteOp, new OperationStepHandler() { @Override public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { try { // Execute the proxy step handler in a separate step // so we have the final response available to our ResultHandler ControllerLogger.MGMT_OP_LOGGER.tracef("sending ModelAddressResolver request %s to remote process using %s", operation, proxyHandler); final AtomicBoolean filtered = new AtomicBoolean(false); context.addStep(new FilterableRemoteOperationStepHandler(proxyHandler, base, filtered, filteredData, ignoreMissing), OperationContext.Stage.MODEL, true); context.completeStep(new OperationContext.ResultHandler() { @Override public void handleResult(OperationContext.ResultAction resultAction, OperationContext context, ModelNode operation) { ControllerLogger.MGMT_OP_LOGGER.tracef("ModelAddressResolver response from remote process is %s", resultItem); if (filtered.get()) { ControllerLogger.MGMT_OP_LOGGER.trace("Response was filtered"); return; } // Determine the address prefix to prepend to RBAC responses from servers PathAddress rbacPrefix = base.size() > 1 && base.getElement(1).getKey().equals(RUNNING_SERVER) ? base : PathAddress.EMPTY_ADDRESS; // If there are multiple targets remaining, the result should be a list if (remaining.isMultiTarget()) { if (resultItem.has(RESULT) && resultItem.get(RESULT).getType() == ModelType.LIST) { for (final ModelNode rr : resultItem.get(RESULT).asList()) { // Create a new result entry final ModelNode nr = result.add(); final PathAddress address = PathAddress.pathAddress(rr.get(OP_ADDR)); // Check whether the result of the remote target contains part of the base address // this might happen for hosts int max = Math.min(base.size(), address.size()); int match = 0; for (int i = 0; i < max; i++) { final PathElement eb = base.getElement(i); final PathElement ea = address.getElement(i); if (eb.getKey().equals(ea.getKey())) { match = i + 1; } } final PathAddress resolvedAddress = base.append(address.subAddress(match)); ControllerLogger.MGMT_OP_LOGGER.tracef("recording multi-target ModelAddressResolver response " + "to %s at %s", fullAddress, resolvedAddress); nr.get(OP_ADDR).set(resolvedAddress.toModelNode()); nr.get(OUTCOME).set(rr.get(OUTCOME)); nr.get(RESULT).set(rr.get(RESULT)); if (rr.hasDefined(RESPONSE_HEADERS)) { ModelNode headers = rr.get(RESPONSE_HEADERS); ModelNode acc = headers.remove(ACCESS_CONTROL); if (headers.asInt() > 0) { nr.get(RESPONSE_HEADERS).set(headers); } if (acc != null && acc.isDefined()) { filteredData.populate(acc, rbacPrefix); ControllerLogger.MGMT_OP_LOGGER.tracef("Populated local filtered data " + "with remote access control headers %s from result item %s", acc, rr); } } } if (resultItem.hasDefined(RESPONSE_HEADERS, ACCESS_CONTROL)) { ModelNode acc = resultItem.get(RESPONSE_HEADERS, ACCESS_CONTROL); filteredData.populate(acc, PathAddress.EMPTY_ADDRESS); } } } else { ControllerLogger.MGMT_OP_LOGGER.tracef("recording non-multi-target ModelAddressResolver response " + "to %s", fullAddress); final ModelNode nr = result.add(); nr.get(OP_ADDR).set(fullAddress.toModelNode()); nr.get(OUTCOME).set(resultItem.get(OUTCOME)); nr.get(RESULT).set(resultItem.get(RESULT)); if (resultItem.hasDefined(RESPONSE_HEADERS)) { ModelNode headers = resultItem.get(RESPONSE_HEADERS); ModelNode acc = headers.remove(ACCESS_CONTROL); if (headers.asInt() > 0) { nr.get(RESPONSE_HEADERS).set(headers); } if (acc != null && acc.isDefined()) { filteredData.populate(acc, PathAddress.EMPTY_ADDRESS); ControllerLogger.MGMT_OP_LOGGER.tracef("Populated local filtered data " + "with remote access control headers %s from result item %s", acc, resultItem); } } } } }); } catch (Resource.NoSuchResourceException e) { // just discard the result to avoid leaking the inaccessible address } } }, OperationContext.Stage.MODEL, true); } } private static final class ModelAddressResolver extends AbstractAddressResolver { public ModelAddressResolver(ModelNode operation, ModelNode result, FilteredData filteredData, OperationStepHandler delegate, FilterPredicate predicate) { super(operation, result, delegate, filteredData, predicate); } protected void executeMultiTargetChildren(PathAddress base, PathElement currentElement, PathAddress newRemaining, OperationContext context, ImmutableManagementResourceRegistration registration, boolean ignoreMissing) { final Resource resource = context.readResource(base, false); final String childType = currentElement.getKey().equals("*") ? null : currentElement.getKey(); if (registration.isRemote()) {// || registration.isRuntimeOnly()) { // At least for proxies it should use the proxy operation handler throw new IllegalStateException(); } // Get the available children final Map<String, Set<String>> resolved = getChildAddresses(context, base, registration, resource, childType); for (Map.Entry<String, Set<String>> entry : resolved.entrySet()) { final String key = entry.getKey(); final Set<String> children = entry.getValue(); if (children.isEmpty()) { continue; } if (currentElement.isWildcard()) { for (final String child : children) { final PathElement e = PathElement.pathElement(key, child); final PathAddress next = base.append(e); // Either require the child or a remote target final ImmutableManagementResourceRegistration nr = context.getResourceRegistration().getSubModel(next); if (resource.hasChild(e) || (nr != null && nr.isRemote())) { safeExecute(next, newRemaining, context, nr, true); } } } else { String[] segments = currentElement.getSegments(); // If there's more than 1 segment, treat that like a wildcard, and don't // fail on bits that disappear boolean ignore = ignoreMissing || segments.length > 1; for (final String segment : currentElement.getSegments()) { if (children.contains(segment)) { final PathElement e = PathElement.pathElement(key, segment); final PathAddress next = base.append(e); // Either require the child or a remote target final ImmutableManagementResourceRegistration nr = context.getResourceRegistration().getSubModel(next); if (resource.hasChild(e) || (nr != null && nr.isRemote())) { safeExecute(next, newRemaining, context, nr, ignore); } } } } } } @Override protected void executeSingleTargetChild(PathAddress base, PathElement currentElement, PathAddress newRemaining, OperationContext context, boolean ignoreMissing) { final PathAddress next = base.append(currentElement); // Either require the child or a remote target final Resource resource = context.readResource(base, false); final ImmutableManagementResourceRegistration nr = context.getResourceRegistration().getSubModel(next); if (resource.hasChild(currentElement) || (nr != null && nr.isRemote())) { safeExecute(next, newRemaining, context, nr, ignoreMissing); } //if we are on the wrong host no need to do anything else if(!resource.hasChild(currentElement)) { throw new Resource.NoSuchResourceException(currentElement); } } @Override protected boolean authorize(OperationContext context, PathAddress base, ModelNode operation) { try { context.readResource(base, false); } catch(UnknowRoleException ex) { context.getFailureDescription().set(ex.getMessage()); return false; } //An exception will happen if not allowed return true; } } private static class FilterableRemoteOperationStepHandler implements OperationStepHandler { private final OperationStepHandler proxyHandler; private final PathAddress base; private final AtomicBoolean filtered; private final FilteredData filteredData; private final boolean ignoreMissing; public FilterableRemoteOperationStepHandler(OperationStepHandler proxyHandler, PathAddress base, AtomicBoolean filtered, FilteredData filteredData, boolean ignoreMissing) { this.proxyHandler = proxyHandler; this.base = base; this.filtered = filtered; this.filteredData = filteredData; this.ignoreMissing = ignoreMissing; } @Override public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { try { proxyHandler.execute(context, operation); ControllerLogger.MGMT_OP_LOGGER.tracef("Preliminary result for %s is %s", operation, context.hasResult() ? context.getResult() : null); } catch (UnauthorizedException e) { // equivalent to the resource not existing // Just report the failure to the filter and complete normally filteredData.addReadRestrictedResource(base); filtered.set(true); ControllerLogger.MGMT_OP_LOGGER.tracef("Caught UnauthorizedException in remote execution from %s", proxyHandler); } catch (ResourceNotAddressableException e) { // Just report the failure to the filter and complete normally filteredData.addAccessRestrictedResource(base); filtered.set(true); ControllerLogger.MGMT_OP_LOGGER.tracef("Caught ResourceNotAddressableException in remote execution from %s", proxyHandler); } catch (Resource.NoSuchResourceException e) { // It's possible this is a remote failure, in which case we // don't get ResourceNotAddressableException. So see if // it was due to any authorization denial ModelNode toAuthorize = operation.clone(); toAuthorize.get(OP).set(READ_RESOURCE_DESCRIPTION_OPERATION); toAuthorize.get(OP_ADDR).set(base.toModelNode()); AuthorizationResult.Decision decision = context.authorize(toAuthorize, EnumSet.of(Action.ActionEffect.ADDRESS)).getDecision(); ControllerLogger.MGMT_OP_LOGGER.tracef("Caught NoSuchResourceException in remote execution from %s. Authorization decision is %s", proxyHandler, decision); if (decision == AuthorizationResult.Decision.DENY) { // Just report the failure to the filter and complete normally filtered.set(true); if (filteredData != null) { filteredData.addAccessRestrictedResource(base); } } else if (!ignoreMissing) { throw e; } } } } private static class RegistrationAddressResolver extends AbstractAddressResolver { RegistrationAddressResolver(final ModelNode operation, final ModelNode result, final OperationStepHandler delegate) { super(operation, result, delegate, null, null); } @Override protected void executeMultiTargetChildren(PathAddress base, PathElement currentElement, PathAddress newRemaining, OperationContext context, ImmutableManagementResourceRegistration registration, boolean ignoreMissing) { final String childType = currentElement.getKey().equals("*") ? null : currentElement.getKey(); if (registration.isRemote()) {// || registration.isRuntimeOnly()) { // At least for proxies it should use the proxy operation handler throw new IllegalStateException(); } final Set<PathElement> children = context.getResourceRegistration().getChildAddresses(base); if (children == null || children.isEmpty()) { throw new NoSuchResourceTypeException(base.append(currentElement)); } boolean foundValid = false; PathAddress invalid = null; for (final PathElement path : children) { if (childType != null && !childType.equals(path.getKey())) { continue; } if (path.getKey().equals(RUNNING_SERVER) && path.isWildcard() && newRemaining.size() > 0) { //Trying to get e.g. /host=xxx/server=*/interface=public will fail, so make sure if there are remaining elements for //a /host=master/server=* that we don't attempt to get those continue; } final PathAddress next = base.append(path); final ImmutableManagementResourceRegistration nr = context.getResourceRegistration().getSubModel(next); try { execute(next, newRemaining, context, nr, ignoreMissing); foundValid = true; } catch (NoSuchResourceTypeException e) { if (!foundValid) { PathAddress failedAddr = e.getPathAddress(); // Store the failed address for error reporting, but only if // 1) this is the first failure, or // 2) The size of the failed address is larger than the currently // cached one, indicating there is some path that has a larger number // of valid elements than the currently cached path. So we want to // report that larger path if (invalid == null || failedAddr.size() > invalid.size()) { PathAddress newBase = base.append(currentElement); invalid = newBase.append(failedAddr.subAddress(newBase.size())); } } } } if (!foundValid) { if (invalid == null) { // No children matched currentElement invalid = base.append(currentElement); } throw new NoSuchResourceTypeException(invalid); } } @Override protected void executeSingleTargetChild(PathAddress base, PathElement currentElement, PathAddress newRemaining, OperationContext context, boolean ignoreMissing) { final PathAddress next = base.append(currentElement); final ImmutableManagementResourceRegistration nr = context.getResourceRegistration().getSubModel(next); if (nr != null) { execute(next, newRemaining, context, nr, ignoreMissing); } else { throw new NoSuchResourceTypeException(next); } } @Override protected boolean authorize(OperationContext context, PathAddress base, ModelNode operation) { if (base.size() > 0) { PathElement element = base.getLastElement(); if (!element.isWildcard() && (element.getKey().equals(HOST)/* || element.getKey().equals(RUNNING_SERVER)*/)) { //Only do this for host resources. The rest of r-r-d does filtering itself. However, without //this: // - a slave host scoped role doing a /host=*:r-r-d will not get rid of the host=master resource // - a master host scoped role doing a /host=*/server=*/subsystem=thing:r-r-d will not get rid of the host=slave resource ModelNode toAuthorize = operation.clone(); toAuthorize.get(OP).set(READ_RESOURCE_DESCRIPTION_OPERATION); toAuthorize.get(OP_ADDR).set(base.toModelNode()); AuthorizationResult.Decision decision = context.authorize(toAuthorize, EnumSet.of(Action.ActionEffect.ADDRESS)).getDecision(); return decision == AuthorizationResult.Decision.PERMIT; } } return true; } } /** * WFCORE-573 Wraps a response to a call where the expected resource may * have disappeared with a flag to record that that has happened. */ static class AvailableResponse { boolean unavailable; final ModelNode response; AvailableResponse(ModelNode response) { this.response = response; } } /** WFCORE-573 Wrap a read-attribute call and handle disappearance of the target resource*/ static class AvailableResponseWrapper implements OperationStepHandler { private final OperationStepHandler wrapped; private final AvailableResponse availableResponse; AvailableResponseWrapper(OperationStepHandler wrapped, AvailableResponse availableResponse) { this.wrapped = wrapped; this.availableResponse = availableResponse; } @Override public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { try { wrapped.execute(context, operation); } catch (Resource.NoSuchResourceException e) { availableResponse.unavailable = true; } } } /** * Gets the addresses of the child resources under the given resource. * * @param context the operation context * @param registry registry entry representing the resource * @param resource the current resource * @param validChildType a single child type to which the results should be limited. If {@code null} the result * should include all child types * @return map where the keys are the child types and the values are a set of child names associated with a type */ static Map<String, Set<String>> getChildAddresses(final OperationContext context, final PathAddress addr, final ImmutableManagementResourceRegistration registry, Resource resource, final String validChildType) { Map<String, Set<String>> result = new HashMap<String, Set<String>>(); Set<PathElement> elements = registry.getChildAddresses(PathAddress.EMPTY_ADDRESS); for (PathElement element : elements) { String childType = element.getKey(); if (validChildType != null && !validChildType.equals(childType)) { continue; } final ImmutableManagementResourceRegistration childRegistration = registry.getSubModel(PathAddress.pathAddress(element)); if (childRegistration == null) { continue; } final AliasEntry aliasEntry = childRegistration.getAliasEntry(); Set<String> set = result.get(childType); if (set == null) { set = new LinkedHashSet<String>(); result.put(childType, set); } if (aliasEntry == null) { if (resource != null && resource.hasChildren(childType)) { Set<String> childNames = resource.getChildrenNames(childType); if (element.isWildcard()) { set.addAll(childNames); } else if (childNames.contains(element.getValue())) { set.add(element.getValue()); } } } else { PathAddress childAddr = addr.append(element); PathAddress target = aliasEntry.convertToTargetAddress(childAddr, AliasContext.create(childAddr, context)); assert !childAddr.equals(target) : "Alias was not translated"; PathAddress targetParent = target.subAddress(0, target.size() - 1); Resource parentResource = context.readResourceFromRoot(targetParent, false); if (parentResource != null) { PathElement targetElement = target.getLastElement(); if (targetElement.isWildcard()) { set.addAll(parentResource.getChildrenNames(targetElement.getKey())); } else if (parentResource.hasChild(targetElement)) { set.add(element.getValue()); } } } if (!element.isWildcard()) { ImmutableManagementResourceRegistration childReg = registry.getSubModel(PathAddress.pathAddress(element)); if (childReg != null && childReg.isRemote()) { set.add(element.getValue()); } } } // WFLY-3306 Ensure we have an entry for any valid child type for (String type : registry.getChildNames(PathAddress.EMPTY_ADDRESS)) { if ((validChildType == null || validChildType.equals(type)) && !result.containsKey(type)) { result.put(type, Collections.<String>emptySet()); } } return result; } static Locale getLocale(OperationContext context, final ModelNode operation) throws OperationFailedException { if (!operation.hasDefined(GlobalOperationAttributes.LOCALE.getName())) { return null; } String unparsed = normalizeLocale(operation.get(GlobalOperationAttributes.LOCALE.getName()).asString()); try { return LocaleResolver.resolveLocale(unparsed); } catch (IllegalArgumentException e) { reportInvalidLocaleFormat(context, e.getMessage()); return null; } } static boolean getRecursive(OperationContext context, ModelNode op) throws OperationFailedException { // -1 means UNDEFINED ModelNode recursiveNode = RECURSIVE.resolveModelAttribute(context, op); final int recursiveValue = recursiveNode.isDefined() ? (recursiveNode.asBoolean() ? 1 : 0) : -1; final int recursiveDepthValue = RECURSIVE_DEPTH.resolveModelAttribute(context, op).asInt(-1); // WFCORE-76: We are recursing in this round IFF: // Recursive is explicitly specified as TRUE and recursiveDepth is UNDEFINED // Recursive is either TRUE or UNDEFINED and recursiveDepth is >0 return recursiveValue > 0 && recursiveDepthValue == -1 || // recursiveValue != 0 && recursiveDepthValue > 0; } static void setNextRecursive(OperationContext context, ModelNode op, ModelNode nextOp) throws OperationFailedException { // -1 means UNDEFINED final int recursiveDepthValue = RECURSIVE_DEPTH.resolveModelAttribute(context, op).asInt(-1); // WFCORE-76: We are recursing in the next step IFF: // Recursive is explicitly specified as TRUE and recursiveDepth is UNDEFINED; or // Recursive is either TRUE or UNDEFINED and (recursiveDepth - 1) is >0 // Recursive value carries through unchanged nextOp.get(RECURSIVE.getName()).set(op.get(RECURSIVE.getName())); switch(recursiveDepthValue) { case -1: // Undefined stays undefined nextOp.get(RECURSIVE_DEPTH.getName()).set(op.get(RECURSIVE_DEPTH.getName())); break; case 0: nextOp.get(RECURSIVE_DEPTH.getName()).set(recursiveDepthValue); break; default: nextOp.get(RECURSIVE_DEPTH.getName()).set(recursiveDepthValue - 1); break; } } private static String normalizeLocale(String toNormalize) { return ("zh_Hans".equalsIgnoreCase(toNormalize) || "zh-Hans".equalsIgnoreCase(toNormalize)) ? "zh_CN" : toNormalize; } private static void reportInvalidLocaleFormat(OperationContext context, String format) { String msg = ControllerLogger.ROOT_LOGGER.invalidLocaleString(format); ControllerLogger.MGMT_OP_LOGGER.debug(msg); // TODO report the problem to client via out-of-band message. // Enable this in 7.2 or later when there is time to test //context.report(MessageSeverity.WARN, msg); } private static final class NoSuchResourceTypeException extends OperationFailedRuntimeException { private final PathAddress pathAddress; private NoSuchResourceTypeException(PathAddress pathAddress) { super(ControllerLogger.ROOT_LOGGER.noSuchResourceType(pathAddress)); this.pathAddress = pathAddress; } private PathAddress getPathAddress() { return pathAddress; } } }