Adobe-Consulting-Services/acs-aem-commons

View on GitHub
oakpal-checks/src/main/java/com/adobe/acs/commons/oakpal/checks/ContentClassifications.java

Summary

Maintainability
A
0 mins
Test Coverage
/*
 * ACS AEM Commons
 *
 * Copyright (C) 2013 - 2023 Adobe
 *
 * 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 com.adobe.acs.commons.oakpal.checks;

import net.adamcin.oakpal.api.JavaxJson;
import net.adamcin.oakpal.api.ProgressCheck;
import net.adamcin.oakpal.api.ProgressCheckFactory;
import net.adamcin.oakpal.api.Rule;
import net.adamcin.oakpal.api.Rules;
import net.adamcin.oakpal.api.Severity;
import net.adamcin.oakpal.api.SimpleProgressCheckFactoryCheck;
import org.apache.jackrabbit.vault.packaging.PackageId;
import org.apache.jackrabbit.vault.util.Text;

import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.json.JsonObject;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import static net.adamcin.oakpal.api.JavaxJson.arrayOrEmpty;
import static net.adamcin.oakpal.api.JavaxJson.optArray;

/**
 * Enforce rules for "Content Classifications" in
 * <a href="https://helpx.adobe.com/experience-manager/6-4/sites/deploying/using/sustainable-upgrades.html#ContentClassifications">Sustainable Upgrades</a>.
 * <p>
 * This check assumes that all the reference nodes under {@code libsPathPrefix} have been provided by forced roots or
 * pre-install packages.
 * <p>
 * {@code config} options:
 * <dl>
 * <dt>{@code libsPathPrefix}</dt>
 * <dd>(default: {@code /libs}) specify where to look for granite protected areas.</dd>
 * <dt>{@code severity}</dt>
 * <dd>(default: {@link net.adamcin.oakpal.api.Severity#MAJOR}) specify the severity of violations reported
 * by this check.</dd>
 * <dt>{@code scopePaths} (type: {@link Rule[]})</dt>
 * <dd>(default: allow all) specify a list of pattern rules to allow or deny import paths from the scope of this check.
 * When no pattern rules are specified, ALLOW ALL imported paths into scope for the check. When the first rule is type
 * ALLOW, the default first rule becomes DENY ALL.
 * </dd>
 * <dt>{@code searchPaths}</dt>
 * <dd>(default: {@code ["/apps","/libs"]}) list of search path prefixes for checking overlays. If null or absent, the
 * default applies.</dd>
 * </dl>
 */
public final class ContentClassifications implements ProgressCheckFactory {
    private static final String P_SLING_RESOURCE_TYPE = "{http://sling.apache.org/jcr/sling/1.0}resourceType";
    private static final String P_SLING_RESOURCE_SUPER_TYPE = "{http://sling.apache.org/jcr/sling/1.0}resourceSuperType";
    private static final String T_GRANITE_PUBLIC_AREA = "{http://www.adobe.com/jcr/granite/1.0}PublicArea";
    private static final String T_GRANITE_ABSTRACT_AREA = "{http://www.adobe.com/jcr/granite/1.0}AbstractArea";
    private static final String T_GRANITE_FINAL_AREA = "{http://www.adobe.com/jcr/granite/1.0}FinalArea";
    private static final String T_GRANITE_INTERNAL_AREA = "{http://www.adobe.com/jcr/granite/1.0}InternalArea";
    @SuppressWarnings("CQRules:CQBP-71")
    private static final String APPS_PATH_PREFIX = "/apps";
    @SuppressWarnings("CQRules:CQBP-71")
    private static final String LIBS_PATH_PREFIX = "/libs";

    private static final String CONFIG_LIBS_PATH_PREFIX = "libsPathPrefix";
    private static final String CONFIG_SEVERITY = "severity";
    private static final String CONFIG_SCOPE_PATHS = "scopePaths";
    private static final String CONFIG_SEARCH_PATHS = "searchPaths";

    @Override
    public ProgressCheck newInstance(final JsonObject jsonObject) {
        final String libsPathPrefix = jsonObject.getString(CONFIG_LIBS_PATH_PREFIX, LIBS_PATH_PREFIX);
        final Severity severity = Severity.valueOf(jsonObject.getString(CONFIG_SEVERITY,
                Severity.MAJOR.name()).toUpperCase());
        final List<Rule> scopePaths = Rules.fromJsonArray(arrayOrEmpty(jsonObject, CONFIG_SCOPE_PATHS));
        final List<String> searchPaths = optArray(jsonObject, CONFIG_SEARCH_PATHS)
                .map(JavaxJson::mapArrayOfStrings).orElse(Arrays.asList(APPS_PATH_PREFIX, LIBS_PATH_PREFIX));

        return new Check(libsPathPrefix, severity, scopePaths, searchPaths);
    }

    static final class Check extends SimpleProgressCheckFactoryCheck<ContentClassifications> {
        final String libsPathPrefix;
        final Severity severity;
        final List<Rule> scopePaths;
        final List<String> searchPaths;

        Check(final String libsPathPrefix, final Severity severity,
              final List<Rule> scopePaths, final List<String> searchPaths) {
            super(ContentClassifications.class);
            this.libsPathPrefix = libsPathPrefix;
            this.severity = severity;
            this.scopePaths = scopePaths;
            this.searchPaths = searchPaths;
        }

        @Override
        public String getCheckName() {
            return ContentClassifications.class.getSimpleName();
        }

        @Override
        public void importedPath(final PackageId packageId, final String path, final Node node)
                throws RepositoryException {

            // short circuit if we happen to be installing /libs content. we won't check for
            // those violations here.
            if (path.startsWith(libsPathPrefix + "/")) {
                return;
            }

            // default to ALLOW ALL
            // if first rule is allow, change default to DENY ALL
            Rule lastMatched = Rules.lastMatch(scopePaths, path);

            // if path is denied from scope, short circuit.
            if (lastMatched.isExclude()) {
                return;
            }

            // check path against libsPathPrefix for overlay
            checkOverlay(packageId, path, node);

            // check sling:resourceType against libsPathPrefix.
            checkResourceType(packageId, path, node);

            // check sling:resourceSuperType against libsPathPrefix.
            checkResourceSuperType(packageId, path, node);

        }

        void checkOverlay(final PackageId packageId, final String path, final Node node)
                throws RepositoryException {
            Optional<String> optPrefix = searchPaths.stream().filter(pre -> path.startsWith(pre + "/")).findFirst();
            if (!optPrefix.isPresent()) {
                return;
            }

            final String searchPrefix = optPrefix.get();
            final String relPath = path.substring(searchPrefix.length(), path.length());
            final String libsRt = libsPathPrefix + relPath;

            assertClassifications(node.getSession(), libsRt, AreaType.ALLOWED_FOR_RESOURCE_SUPER_TYPE)
                    .ifPresent(message ->
                            reporting(violation -> violation
                                    .withSeverity(severity)
                                    .withPackage(packageId)
                                    .withDescription("{0} [restricted overlay]: {1}")
                                    .withArgument(path, message)));

        }

        void checkResourceType(final PackageId packageId, final String path, final Node node)
                throws RepositoryException {
            if (node.hasProperty(P_SLING_RESOURCE_TYPE)) {
                String rt = node.getProperty(P_SLING_RESOURCE_TYPE).getString();
                if (rt.length() > 0) {
                    final String libsRt;
                    if (rt.startsWith("/")) {
                        libsRt = rt;
                    } else {
                        libsRt = libsPathPrefix + "/" + rt;
                    }

                    assertClassifications(node.getSession(), libsRt, AreaType.ALLOWED_FOR_RESOURCE_TYPE)
                            .ifPresent(message ->
                                    reporting(violation -> violation
                                            .withSeverity(severity)
                                            .withPackage(packageId)
                                            .withDescription("{0} [restricted resource type]: {1}")
                                            .withArgument(path, message)));
                }
            }
        }

        void checkResourceSuperType(final PackageId packageId, final String path, final Node node)
                throws RepositoryException {
            if (node.hasProperty(P_SLING_RESOURCE_SUPER_TYPE)) {
                String rst = node.getProperty(P_SLING_RESOURCE_SUPER_TYPE).getString();
                if (rst.length() > 0) {
                    final String libsRst;
                    if (rst.startsWith("/")) {
                        libsRst = rst;
                    } else {
                        libsRst = libsPathPrefix + "/" + rst;
                    }

                    assertClassifications(node.getSession(), libsRst, AreaType.ALLOWED_FOR_RESOURCE_SUPER_TYPE)
                            .ifPresent(message ->
                                    reporting(violation -> violation
                                            .withSeverity(severity)
                                            .withPackage(packageId)
                                            .withDescription("{0} [restricted super type]: {1}")
                                            .withArgument(path, message)));
                }
            }
        }

        Optional<String> assertClassifications(final Session session, final String libsPath,
                                               final Set<AreaType> allowedAreas) throws RepositoryException {
            if (libsPath.startsWith(libsPathPrefix)) {
                final Node leaf = getLeafNode(session, libsPath);
                final AreaType leafArea = AreaType.fromNode(leaf);
                if (leafArea == AreaType.FINAL && libsPath.startsWith(leaf.getPath() + "/")) {
                    return Optional.of(MessageFormat.format(getString("{0} is implicitly marked {1}"),
                            libsPath, AreaType.INTERNAL));
                }
                if (!allowedAreas.contains(leafArea)) {
                    return Optional.of(MessageFormat.format(getString("{0} is marked {1}"),
                            leaf.getPath(), leafArea));
                }
            }

            return Optional.empty();
        }

        Node getLeafNode(final Session session, final String absPath) throws RepositoryException {
            if ("/".equals(absPath)) {
                return session.getRootNode();
            } else {
                final String parentPath = Text.getRelativeParent(absPath, 1, true);
                final Node parent = getLeafNode(session, parentPath);
                final String name = Text.getName(absPath, true);
                if (parent.getPath().equals(parentPath) && parent.hasNode(name)) {
                    return parent.getNode(name);
                } else {
                    return parent;
                }
            }
        }
    }

    public enum AreaType {
        PUBLIC(T_GRANITE_PUBLIC_AREA),
        ABSTRACT(T_GRANITE_ABSTRACT_AREA),
        FINAL(T_GRANITE_FINAL_AREA),
        INTERNAL(T_GRANITE_INTERNAL_AREA);
        final String mixinType;

        AreaType(final String mixinType) {
            this.mixinType = mixinType;
        }

        protected static final Set<AreaType> ALLOWED_FOR_RESOURCE_TYPE =
                Collections.unmodifiableSet(new HashSet<>(Arrays.asList(PUBLIC, FINAL)));
        protected static final Set<AreaType> ALLOWED_FOR_RESOURCE_SUPER_TYPE =
                Collections.unmodifiableSet(new HashSet<>(Arrays.asList(PUBLIC, ABSTRACT)));

        public static AreaType fromNode(final Node node) throws RepositoryException {
            for (AreaType value : values()) {
                if (node.isNodeType(value.mixinType)) {
                    return value;
                }
            }
            if (node.getSession().getRootNode().isSame(node)) {
                return PUBLIC;
            } else {
                AreaType parentType = fromNode(node.getParent());
                if (parentType == FINAL) {
                    return INTERNAL;
                } else {
                    return parentType;
                }
            }
        }
    }
}