This is an automated email from the ASF dual-hosted git repository. angela pushed a commit to branch SLING-10219 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-cpconverter.git
commit 8ae335d87fa650f5aa3ffd99da5d4a693fcc5eba Author: angela <[email protected]> AuthorDate: Thu Mar 18 18:30:57 2021 +0100 SLING-10219 : Expand enforcing principal-based authorization option to repo-init (initial draft) --- pom.xml | 12 +- .../cpconverter/accesscontrol/AclManager.java | 3 + .../accesscontrol/DefaultAclManager.java | 109 +++++++++-- .../cpconverter/accesscontrol/EnforceInfo.java | 29 +++ .../AbstractConfigurationEntryHandler.java | 8 +- .../handlers/NodeTypesEntryHandler.java | 17 +- .../cpconverter/repoinit/AccessControlVisitor.java | 198 ++++++++++++++++++++ .../cpconverter/repoinit/ConversionMap.java | 81 +++++++++ .../cpconverter/repoinit/DefaultVisitor.java | 198 ++++++++++++++++++++ .../feature/cpconverter/repoinit/NoOpVisitor.java | 118 ++++++++++++ .../cpconverter/repoinit/OperationProcessor.java | 53 ++++++ .../cpconverter/repoinit/SystemUserVisitor.java | 52 ++++++ .../feature/cpconverter/shared/NodeTypeUtil.java | 47 +++++ .../handlers/ConfigurationEntryHandlerTest.java | 3 +- .../feature/cpconverter/handlers/RepoInitTest.java | 201 +++++++++++++++++++++ ....RepositoryInitializer-conversion-result.config | 45 +++++ ...it.RepositoryInitializer-conversion-test.config | 39 ++++ ....RepositoryInitializer-no-conv-with-diff.config | 34 ++++ ...RepositoryInitializer-no-conversion-test.config | 72 ++++++++ 19 files changed, 1279 insertions(+), 40 deletions(-) diff --git a/pom.xml b/pom.xml index c97c1d0..d82a3d7 100644 --- a/pom.xml +++ b/pom.xml @@ -122,6 +122,12 @@ <scope>provided</scope> </dependency> <dependency> + <groupId>org.apache.sling</groupId> + <artifactId>org.apache.sling.repoinit.parser</artifactId> + <version>1.6.6</version> + <scope>provided</scope> + </dependency> + <dependency> <groupId>org.apache.felix</groupId> <artifactId>org.apache.felix.converter</artifactId> <version>1.0.14</version> @@ -241,12 +247,6 @@ <version>${mockito-core.version}</version> <scope>test</scope> </dependency> - <dependency> - <groupId>org.apache.sling</groupId> - <artifactId>org.apache.sling.repoinit.parser</artifactId> - <version>1.6.4</version> - <scope>test</scope> - </dependency> </dependencies> <build> diff --git a/src/main/java/org/apache/sling/feature/cpconverter/accesscontrol/AclManager.java b/src/main/java/org/apache/sling/feature/cpconverter/accesscontrol/AclManager.java index 95f2f75..4c81b91 100644 --- a/src/main/java/org/apache/sling/feature/cpconverter/accesscontrol/AclManager.java +++ b/src/main/java/org/apache/sling/feature/cpconverter/accesscontrol/AclManager.java @@ -22,6 +22,7 @@ import org.apache.jackrabbit.vault.fs.spi.PrivilegeDefinitions; import org.apache.sling.feature.cpconverter.features.FeaturesManager; import org.apache.sling.feature.cpconverter.vltpkg.VaultPackageAssembler; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * The Manager able to collect and build System Users and related ACL policies. @@ -40,6 +41,8 @@ public interface AclManager { void addRepoinitExtension(@NotNull List<VaultPackageAssembler> packageAssemblers, @NotNull FeaturesManager featureManager); + void addRepoinitExtention(@Nullable String repoInitText, @Nullable String runMode, @NotNull FeaturesManager featuresManager); + void addNodetypeRegistrationSentence(@NotNull String nodetypeRegistrationSentence); void addPrivilegeDefinitions(@NotNull PrivilegeDefinitions privilegeDefinitions); diff --git a/src/main/java/org/apache/sling/feature/cpconverter/accesscontrol/DefaultAclManager.java b/src/main/java/org/apache/sling/feature/cpconverter/accesscontrol/DefaultAclManager.java index ce4b708..a399bb0 100644 --- a/src/main/java/org/apache/sling/feature/cpconverter/accesscontrol/DefaultAclManager.java +++ b/src/main/java/org/apache/sling/feature/cpconverter/accesscontrol/DefaultAclManager.java @@ -21,10 +21,16 @@ import org.apache.jackrabbit.spi.PrivilegeDefinition; import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; import org.apache.jackrabbit.spi.commons.conversion.NameResolver; import org.apache.jackrabbit.vault.fs.spi.PrivilegeDefinitions; +import org.apache.jackrabbit.vault.util.PathUtil; import org.apache.jackrabbit.vault.util.PlatformNameFormat; +import org.apache.jackrabbit.vault.util.Text; import org.apache.sling.feature.cpconverter.features.FeaturesManager; +import org.apache.sling.feature.cpconverter.repoinit.OperationProcessor; import org.apache.sling.feature.cpconverter.shared.RepoPath; import org.apache.sling.feature.cpconverter.vltpkg.VaultPackageAssembler; +import org.apache.sling.repoinit.parser.RepoInitParsingException; +import org.apache.sling.repoinit.parser.impl.RepoInitParserService; +import org.apache.sling.repoinit.parser.operations.Operation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -33,11 +39,11 @@ import org.slf4j.LoggerFactory; import javax.jcr.NamespaceException; import java.io.File; import java.io.FileInputStream; +import java.io.StringReader; import java.util.Collection; -import java.util.HashSet; -import java.util.Optional; import java.util.Formatter; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; @@ -45,18 +51,20 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; -public class DefaultAclManager implements AclManager { +public class DefaultAclManager implements AclManager, EnforceInfo { private static final Logger log = LoggerFactory.getLogger(DefaultAclManager.class); private static final String CONTENT_XML_FILE_NAME = ".content.xml"; private final RepoPath enforcePrincipalBasedSupportedPath; + private final OperationProcessor processor = new OperationProcessor(); private final Set<SystemUser> systemUsers = new LinkedHashSet<>(); private final Set<Group> groups = new LinkedHashSet<>(); @@ -149,6 +157,29 @@ public class DefaultAclManager implements AclManager { } } + @Override + public void addRepoinitExtention(@Nullable String repoInitText, @Nullable String runMode, @NotNull FeaturesManager featuresManager) { + if (repoInitText == null || repoInitText.trim().isEmpty()) { + return; + } + + try (Formatter formatter = new Formatter()) { + if (enforcePrincipalBased()) { + List<Operation> ops = new RepoInitParserService().parse(new StringReader(repoInitText)); + processor.apply(ops, formatter,this); + } else { + formatter.format("%s", repoInitText); + } + + String text = formatter.toString().trim(); + if (!text.isEmpty()) { + featuresManager.addOrAppendRepoInitExtension(text, runMode); + } + } catch (RepoInitParsingException e) { + throw new IllegalStateException(e); + } + } + private void addUsersAndGroups(@NotNull Formatter formatter) { for (SystemUser systemUser : systemUsers) { // make sure all system users are created first @@ -172,16 +203,8 @@ public class DefaultAclManager implements AclManager { @NotNull private String calculateIntermediatePath(@NotNull SystemUser systemUser) { RepoPath intermediatePath = systemUser.getIntermediatePath(); - if (enforcePrincipalBased(systemUser) && !intermediatePath.startsWith(enforcePrincipalBasedSupportedPath)) { - RepoPath parent = intermediatePath.getParent(); - while (parent != null) { - if (enforcePrincipalBasedSupportedPath.startsWith(parent)) { - String relpath = intermediatePath.toString().substring(parent.toString().length()); - return enforcePrincipalBasedSupportedPath.toString() + relpath; - } - parent = parent.getParent(); - } - throw new IllegalStateException("Cannot calculate intermediate path for service user. Configured Supported path " +enforcePrincipalBasedSupportedPath+" has no common ancestor with "+intermediatePath); + if (enforcePrincipalBased(systemUser)) { + return calculateEnforcedIntermediatePath(intermediatePath.toString()); } else { return intermediatePath.toString(); } @@ -248,17 +271,20 @@ public class DefaultAclManager implements AclManager { } } + private boolean enforcePrincipalBased() { + return enforcePrincipalBasedSupportedPath != null; + } + private boolean enforcePrincipalBased(@NotNull SystemUser systemUser) { - if (enforcePrincipalBasedSupportedPath == null) { - return false; - } else { + if (enforcePrincipalBased()) { if (mappedById.contains(systemUser.getId())) { - log.warn("Skip enforcing principalbased access control setup for system user {} due to existing mapping", systemUser.getId()); + log.warn("Skip enforcing principal-based access control setup for system user '{}' due to existing mapping by id.", systemUser.getId()); return false; } else { return true; } } + return false; } private void writeEntry(@NotNull AccessControlEntry entry, @NotNull String path, @NotNull Formatter formatter) { @@ -296,6 +322,55 @@ public class DefaultAclManager implements AclManager { privilegeDefinitions = null; } + @Override + public boolean enforcePrincipalBased(@NotNull String serviceUserId) { + // FIXME get rid of duplication with enforePrincipalBased(SystemUser) + // FIXME get rid of streming mappings multiple times + if (enforcePrincipalBased()) { + if (mappings.stream().anyMatch(mapping -> mapping.mapsUser(serviceUserId))) { + log.warn("Skip enforcing principal-based access control setup for system user '{}' due to existing mapping by id.", serviceUserId); + return false; + } else { + return true; + } + } + return false; + } + + @Override + @NotNull + public String calculateEnforcedIntermediatePath(@Nullable String intermediatePath) { + if (enforcePrincipalBasedSupportedPath == null) { + return intermediatePath; + } + String supportedPath = enforcePrincipalBasedSupportedPath.toString(); + if (intermediatePath == null || intermediatePath.isEmpty()) { + return enforcePrincipalBasedSupportedPath.toString(); + } else if (Text.isDescendantOrEqual(supportedPath, intermediatePath)) { + return intermediatePath; + } else if (!intermediatePath.startsWith("/")) { + String p = "/"+intermediatePath; + while (!p.isEmpty()) { + if (supportedPath.contains(p)) { + String relpath = intermediatePath.substring(p.length()); + return supportedPath + "/" +relpath; + } + p = Text.getRelativeParent(p, 1); + } + return supportedPath + intermediatePath; + } else { + String parent = Text.getRelativeParent(intermediatePath, 1); + while (!parent.isEmpty() && !"/".equals(parent)) { + if (Text.isDescendantOrEqual(parent, supportedPath)) { + String relpath = intermediatePath.substring(parent.length()); + return supportedPath + relpath; + } + parent = Text.getRelativeParent(parent, 1); + } + throw new IllegalStateException("Cannot calculate intermediate path for service user. Configured Supported path " +enforcePrincipalBasedSupportedPath+" has no common ancestor with "+intermediatePath); + } + } + protected @Nullable String computePathWithTypes(@NotNull RepoPath path, @NotNull List<VaultPackageAssembler> packageAssemblers) { boolean foundType = false; String repoinitPath = "/"; diff --git a/src/main/java/org/apache/sling/feature/cpconverter/accesscontrol/EnforceInfo.java b/src/main/java/org/apache/sling/feature/cpconverter/accesscontrol/EnforceInfo.java new file mode 100644 index 0000000..dc57f3b --- /dev/null +++ b/src/main/java/org/apache/sling/feature/cpconverter/accesscontrol/EnforceInfo.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sling.feature.cpconverter.accesscontrol; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public interface EnforceInfo { + + boolean enforcePrincipalBased(@NotNull String serviceUserId); + + @NotNull + String calculateEnforcedIntermediatePath(@Nullable String intermediatePath); + +} \ No newline at end of file diff --git a/src/main/java/org/apache/sling/feature/cpconverter/handlers/AbstractConfigurationEntryHandler.java b/src/main/java/org/apache/sling/feature/cpconverter/handlers/AbstractConfigurationEntryHandler.java index 22bb540..7d56175 100644 --- a/src/main/java/org/apache/sling/feature/cpconverter/handlers/AbstractConfigurationEntryHandler.java +++ b/src/main/java/org/apache/sling/feature/cpconverter/handlers/AbstractConfigurationEntryHandler.java @@ -29,6 +29,7 @@ import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter import org.apache.sling.feature.cpconverter.accesscontrol.AclManager; import org.apache.sling.feature.cpconverter.accesscontrol.Mapping; import org.apache.sling.feature.cpconverter.features.FeaturesManager; +import org.apache.sling.feature.cpconverter.repoinit.OperationProcessor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.osgi.util.converter.Converters; @@ -98,10 +99,9 @@ abstract class AbstractConfigurationEntryHandler extends AbstractRegexEntryHandl if (REPOINIT_FACTORY_PID.equals(factoryPid)) { final String[] scripts = Converters.standardConverter().convert(configurationProperties.get("scripts")).to(String[].class); if (scripts != null && scripts.length > 0 ) { - for(final String text : scripts) { - if ( text != null && !text.trim().isEmpty() ) { - featuresManager.addOrAppendRepoInitExtension(text, runMode); - } + AclManager aclManager = Objects.requireNonNull(converter.getAclManager()); + for (final String text : scripts) { + aclManager.addRepoinitExtention(text, runMode, featuresManager); } } checkReferences(configurationProperties, pid); diff --git a/src/main/java/org/apache/sling/feature/cpconverter/handlers/NodeTypesEntryHandler.java b/src/main/java/org/apache/sling/feature/cpconverter/handlers/NodeTypesEntryHandler.java index 3d0d7c4..7b898f9 100644 --- a/src/main/java/org/apache/sling/feature/cpconverter/handlers/NodeTypesEntryHandler.java +++ b/src/main/java/org/apache/sling/feature/cpconverter/handlers/NodeTypesEntryHandler.java @@ -25,6 +25,8 @@ import org.apache.jackrabbit.vault.fs.io.Archive; import org.apache.jackrabbit.vault.fs.io.Archive.Entry; import org.apache.jackrabbit.vault.util.Constants; import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter; +import org.apache.sling.feature.cpconverter.accesscontrol.AclManager; +import org.apache.sling.feature.cpconverter.shared.NodeTypeUtil; import org.jetbrains.annotations.NotNull; public class NodeTypesEntryHandler extends AbstractRegexEntryHandler { @@ -54,19 +56,10 @@ public class NodeTypesEntryHandler extends AbstractRegexEntryHandler { public void handle(@NotNull String path, @NotNull Archive archive, @NotNull Entry entry, @NotNull ContentPackage2FeatureModelConverter converter) throws Exception { try (BufferedReader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(archive.openInputStream(entry))))) { - Objects.requireNonNull(converter.getAclManager()).addNodetypeRegistrationSentence("register nodetypes"); - Objects.requireNonNull(converter.getAclManager()).addNodetypeRegistrationSentence("<<==="); - - String nodetypeRegistrationSentence; - while ((nodetypeRegistrationSentence = reader.readLine()) != null) { - if (nodetypeRegistrationSentence.isEmpty()) { - converter.getAclManager().addNodetypeRegistrationSentence(""); - } else { - converter.getAclManager().addNodetypeRegistrationSentence("<< " + nodetypeRegistrationSentence); - } + AclManager aclManager = Objects.requireNonNull(converter.getAclManager()); + for (String line : NodeTypeUtil.generateRepoInitLines(reader)) { + aclManager.addNodetypeRegistrationSentence(line); } - - converter.getAclManager().addNodetypeRegistrationSentence("===>>"); } } diff --git a/src/main/java/org/apache/sling/feature/cpconverter/repoinit/AccessControlVisitor.java b/src/main/java/org/apache/sling/feature/cpconverter/repoinit/AccessControlVisitor.java new file mode 100644 index 0000000..87178b3 --- /dev/null +++ b/src/main/java/org/apache/sling/feature/cpconverter/repoinit/AccessControlVisitor.java @@ -0,0 +1,198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sling.feature.cpconverter.repoinit; + +import org.apache.sling.feature.cpconverter.accesscontrol.EnforceInfo; +import org.apache.sling.repoinit.parser.operations.AclLine; +import org.apache.sling.repoinit.parser.operations.RestrictionClause; +import org.apache.sling.repoinit.parser.operations.SetAclPaths; +import org.apache.sling.repoinit.parser.operations.SetAclPrincipalBased; +import org.apache.sling.repoinit.parser.operations.SetAclPrincipals; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Formatter; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +class AccessControlVisitor extends NoOpVisitor { + + private final Formatter formatter; + private final EnforceInfo enforceInfo; + private final ConversionMap toConvert; + private final Set<String> systemUserIds; + + AccessControlVisitor(@NotNull Formatter formatter, @NotNull EnforceInfo enforceInfo, + @NotNull ConversionMap toConvert, @NotNull Set<String> systemUserIds) { + this.formatter = formatter; + this.enforceInfo = enforceInfo; + this.toConvert = toConvert; + this.systemUserIds = systemUserIds; + } + + @Override + public void visitSetAclPrincipal(SetAclPrincipals setAclPrincipals) { + List<String> principalNames = new ArrayList<>(setAclPrincipals.getPrincipals()); + String optionString = getAclOptionsString(setAclPrincipals.getOptions()); + for (String principalName : setAclPrincipals.getPrincipals()) { + if (enforcePrincipalBased(principalName)) { + toConvert.putAll(principalName, optionString, setAclPrincipals.getLines()); + principalNames.remove(principalName); + } + } + // re-create original repo-init statements for all principals that don't get converted + Collection<AclLine> lines = setAclPrincipals.getLines(); + if (!principalNames.isEmpty() && !lines.isEmpty()) { + if (lines.stream().anyMatch(aclLine -> { + List<String> paths = aclLine.getProperty(AclLine.PROP_PATHS); + return paths == null || paths.isEmpty(); + })) { + generateRepoInit(formatter, "set repository ACL for %s%s%n", true, listToString(setAclPrincipals.getPrincipals()), optionString, lines); + } else { + generateRepoInit(formatter, "set ACL for %s%s%n", true, listToString(setAclPrincipals.getPrincipals()), optionString, lines); + } + } + } + + @Override + public void visitSetAclPaths(SetAclPaths setAclPaths) { + String optionString = getAclOptionsString(setAclPaths.getOptions()); + List<AclLine> lines = new ArrayList<>(); + for (AclLine line : setAclPaths.getLines()) { + List<String> principalNames = new ArrayList<>(line.getProperty(AclLine.PROP_PRINCIPALS)); + for (String principalName : line.getProperty(AclLine.PROP_PRINCIPALS)) { + if (enforcePrincipalBased(principalName)) { + AclLine newLine = createAclLine(line, null, setAclPaths.getPaths()); + toConvert.put(principalName, optionString, newLine); + principalNames.remove(principalName); + } + } + + if (principalNames.equals(line.getProperty(AclLine.PROP_PRINCIPALS))) { + // nothing to convert -> use the original line + lines.add(line); + } else if (!principalNames.isEmpty()) { + // re-create modified ACLLine without the principals that will be converted + AclLine modified = createAclLine(line, principalNames, null); + lines.add(modified); + } + } + + if (!lines.isEmpty()) { + generateRepoInit(formatter, "set ACL on %s%s%n", false, pathsToString(setAclPaths.getPaths()), optionString, lines); + } + } + + @Override + public void visitSetAclPrincipalBased(SetAclPrincipalBased setAclPrincipalBased) { + generateRepoInit(formatter, "set principal ACL for %s%s%n", true, listToString(setAclPrincipalBased.getPrincipals()), getAclOptionsString(setAclPrincipalBased.getOptions()), setAclPrincipalBased.getLines()); + } + + private boolean enforcePrincipalBased(@NotNull String principalName) { + return systemUserIds.contains(principalName) && enforceInfo.enforcePrincipalBased(principalName); + } + + static void generateRepoInit(@NotNull Formatter formatter, @NotNull String start, boolean hasPathLines, @NotNull String principalsOrPaths, + @NotNull String optionString, @NotNull Collection<AclLine> lines) { + formatter.format(start, principalsOrPaths, optionString); + for (AclLine line : lines) { + String action = actionToString(line.getAction()); + String privileges = privilegesToString(line.getAction(), line.getProperty(AclLine.PROP_PRIVILEGES)); + String onOrFor; + if (hasPathLines) { + String pathStr = pathsToString(line.getProperty(AclLine.PROP_PATHS)); + onOrFor = (pathStr.isEmpty()) ? "" : " on " + pathStr; + } else { + onOrFor = " for " + listToString(line.getProperty(AclLine.PROP_PRINCIPALS)); + } + formatter.format(" %s %s%s%s%s%n", action, privileges, onOrFor, + nodetypesToString(line.getProperty(AclLine.PROP_NODETYPES)), + restrictionsToString(line.getRestrictions())); + } + formatter.format("end%n"); + } + + @NotNull + private static AclLine createAclLine(@NotNull AclLine base, @Nullable List<String> principalNames, @Nullable List<String> paths) { + AclLine al = new AclLine(base.getAction()); + if (principalNames != null) { + al.setProperty(AclLine.PROP_PRINCIPALS, principalNames); + } + if (paths != null && !paths.isEmpty()) { + al.setProperty(AclLine.PROP_PATHS, paths); + } + al.setProperty(AclLine.PROP_PRIVILEGES, base.getProperty(AclLine.PROP_PRIVILEGES)); + al.setProperty(AclLine.PROP_NODETYPES, base.getProperty(AclLine.PROP_NODETYPES)); + al.setRestrictions(base.getRestrictions()); + return al; + } + + @NotNull + private static String getAclOptionsString(@NotNull List<String> options) { + return (options.isEmpty()) ? "" : " (ACLOptions="+ listToString(options)+")"; + } + + @NotNull + private static String privilegesToString(@NotNull AclLine.Action action, @NotNull List<String> privileges) { + return (action == AclLine.Action.REMOVE_ALL) ? "*" : listToString(privileges); + } + + @NotNull + private static String pathsToString(@NotNull List<String> paths) { + return listToString(paths.stream() + .map(s -> { + String homestr = ":home:"; + if (s.startsWith(homestr)) { + return "home(" + s.substring(homestr.length(), s.lastIndexOf('#')) +")"; + } else { + return s; + } + }) + .collect(Collectors.toList())); + } + + @NotNull + private static String nodetypesToString(@NotNull List<String> nodetypes) { + return (nodetypes.isEmpty()) ? "" : " nodetypes " + listToString(nodetypes); + } + + @NotNull + private static String restrictionsToString(@NotNull List<RestrictionClause> restrictionClauses) { + StringBuilder sb = new StringBuilder(); + for (RestrictionClause rc : restrictionClauses) { + sb.append(" restriction(").append(rc.getName()); + for (String v : rc.getValues()) { + sb.append(",").append(v); + } + sb.append(')'); + } + return sb.toString(); + } + + @NotNull + private static String actionToString(@NotNull AclLine.Action action) { + switch (action) { + case DENY: return "deny"; + case REMOVE: return "remove"; + case REMOVE_ALL: return "remove"; + default: return "allow"; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/apache/sling/feature/cpconverter/repoinit/ConversionMap.java b/src/main/java/org/apache/sling/feature/cpconverter/repoinit/ConversionMap.java new file mode 100644 index 0000000..65f6694 --- /dev/null +++ b/src/main/java/org/apache/sling/feature/cpconverter/repoinit/ConversionMap.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sling.feature.cpconverter.repoinit; + +import org.apache.sling.repoinit.parser.operations.AclLine; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Formatter; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +class ConversionMap { + + private final Map<Key, List<AclLine>> map = new LinkedHashMap<>(); + + void put(@NotNull String principalName, @NotNull String options, @NotNull AclLine line) { + List<AclLine> lineList = map.computeIfAbsent(new Key(principalName, options), k -> new ArrayList<>()); + lineList.add(line); + } + + void putAll(@NotNull String principalName, @NotNull String options, @NotNull Collection<AclLine> lines) { + List<AclLine> lineList = map.computeIfAbsent(new Key(principalName, options), k -> new ArrayList<>()); + lineList.addAll(lines); + } + + void generateRepoInit(@NotNull Formatter formatter) { + for (Map.Entry<Key, List<AclLine>> entry : map.entrySet()) { + String principalName = entry.getKey().principalName; + String options = entry.getKey().options; + AccessControlVisitor.generateRepoInit(formatter, "set principal ACL for %s%s%n", true, principalName, options, entry.getValue()); + } + map.clear(); + } + + private static final class Key { + private final String principalName; + private final String options; + + private Key(@NotNull String principalName, @NotNull String options) { + this.principalName = principalName; + this.options = options; + } + + @Override + public int hashCode() { + return Objects.hash(principalName, options); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Key) { + Key other = (Key) obj; + return principalName.equals(other.principalName) && options.equals(other.options); + } else { + return false; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/apache/sling/feature/cpconverter/repoinit/DefaultVisitor.java b/src/main/java/org/apache/sling/feature/cpconverter/repoinit/DefaultVisitor.java new file mode 100644 index 0000000..b2e142e --- /dev/null +++ b/src/main/java/org/apache/sling/feature/cpconverter/repoinit/DefaultVisitor.java @@ -0,0 +1,198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sling.feature.cpconverter.repoinit; + +import org.apache.jackrabbit.util.ISO8601; +import org.apache.sling.feature.cpconverter.shared.NodeTypeUtil; +import org.apache.sling.repoinit.parser.operations.AddGroupMembers; +import org.apache.sling.repoinit.parser.operations.CreateGroup; +import org.apache.sling.repoinit.parser.operations.CreatePath; +import org.apache.sling.repoinit.parser.operations.CreateUser; +import org.apache.sling.repoinit.parser.operations.DeleteGroup; +import org.apache.sling.repoinit.parser.operations.DeleteServiceUser; +import org.apache.sling.repoinit.parser.operations.DeleteUser; +import org.apache.sling.repoinit.parser.operations.DisableServiceUser; +import org.apache.sling.repoinit.parser.operations.PathSegmentDefinition; +import org.apache.sling.repoinit.parser.operations.PropertyLine; +import org.apache.sling.repoinit.parser.operations.RegisterNamespace; +import org.apache.sling.repoinit.parser.operations.RegisterNodetypes; +import org.apache.sling.repoinit.parser.operations.RegisterPrivilege; +import org.apache.sling.repoinit.parser.operations.RemoveGroupMembers; +import org.apache.sling.repoinit.parser.operations.SetProperties; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.Calendar; +import java.util.Formatter; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +class DefaultVisitor extends NoOpVisitor { + + private final Formatter formatter; + + DefaultVisitor(@NotNull Formatter formatter) { + this.formatter = formatter; + } + + @Override + public void visitCreateGroup(@NotNull CreateGroup createGroup) { + String path = createGroup.getPath(); + if (path == null || path.isEmpty()) { + formatter.format("create group %s%n", createGroup.getGroupname()); + } else { + String forced = (createGroup.isForcedPath()) ? "forced " : ""; + formatter.format("create group %s with %spath %s%n", createGroup.getGroupname(), forced, path); + } + } + + @Override + public void visitDeleteGroup(@NotNull DeleteGroup deleteGroup) { + formatter.format("delete group %s%n", deleteGroup.getGroupname()); + } + + @Override + public void visitCreateUser(@NotNull CreateUser createUser) { + String path = createUser.getPath(); + if (path == null || path.isEmpty()) { + formatter.format("create user %s%s%n", createUser.getUsername(), getPwString(createUser)); + } else { + String forced = (createUser.isForcedPath()) ? "forced " : ""; + formatter.format("create user %s with %spath %s%s%n", createUser.getUsername(), forced, path, getPwString(createUser)); + } + } + + @NotNull + private static String getPwString(@NotNull CreateUser createUser) { + String pw = createUser.getPassword(); + if (pw == null || pw.isEmpty()) { + return ""; + } else { + String enc = (createUser.getPasswordEncoding() != null) ? "{"+createUser.getPasswordEncoding()+"} " : ""; + return " with password "+ enc + createUser.getPassword(); + } + } + + @Override + public void visitDeleteUser(DeleteUser deleteUser) { + formatter.format("delete user %s%n", deleteUser.getUsername()); + } + + @Override + public void visitDeleteServiceUser(DeleteServiceUser deleteServiceUser) { + formatter.format("delete service user %s%n", deleteServiceUser.getUsername()); + } + + @Override + public void visitCreatePath(CreatePath createPath) { + // FIXME: see SLING-10231 + // the CreatePath operation doesn't allow to retrieve the default primary type + // therefore the generated statement may not be identical to the original one. + StringBuilder sb = new StringBuilder(); + for (PathSegmentDefinition psd : createPath.getDefinitions()) { + sb.append("/").append(psd.getSegment()).append("(").append(psd.getPrimaryType()); + List<String> mixins = psd.getMixins(); + if (mixins != null && !mixins.isEmpty()) { + sb.append(" mixin ").append(listToString(mixins)); + } + sb.append(")"); + } + formatter.format("create path %s%n", sb.toString()); + } + + @Override + public void visitRegisterNamespace(RegisterNamespace registerNamespace) { + formatter.format("register namespace ( %s ) %s%n", registerNamespace.getPrefix(), registerNamespace.getURI()); + } + + @Override + public void visitRegisterNodetypes(RegisterNodetypes registerNodetypes) { + try { + for (String nodetypeRegistrationSentence : NodeTypeUtil.generateRepoInitLines(new BufferedReader(new StringReader(registerNodetypes.getCndStatements())))) { + formatter.format("%s%n", nodetypeRegistrationSentence); + } + } catch (IOException e) { + throw new IllegalStateException(e.getMessage()); + } + } + + @Override + public void visitRegisterPrivilege(RegisterPrivilege registerPrivilege) { + formatter.format("%s%n", registerPrivilege.toString()); + } + + @Override + public void visitDisableServiceUser(DisableServiceUser disableServiceUser) { + // FIXME : see SLING-10235 + String reason = disableServiceUser.getParametersDescription(); + String id = disableServiceUser.getUsername(); + if (reason.startsWith(id + " : ")) { + reason = reason.substring((id + " : ").length()); + } + formatter.format("disable service user %s : \"%s\"%n", disableServiceUser.getUsername(), reason); + + } + + @Override + public void visitAddGroupMembers(AddGroupMembers addGroupMembers) { + formatter.format("add %s to group %s%n", listToString(addGroupMembers.getMembers()), addGroupMembers.getGroupname()); + } + + @Override + public void visitRemoveGroupMembers(RemoveGroupMembers removeGroupMembers) { + formatter.format("remove %s from group %s%n", listToString(removeGroupMembers.getMembers()), removeGroupMembers.getGroupname()); + } + + @Override + public void visitSetProperties(SetProperties setProperties) { + // FIXME: see SLING-10238 for type and quoted values that cannot be generated + // exactly as they were originally defined in repo-init + formatter.format("set properties on %s%n", listToString(setProperties.getPaths())); + for (PropertyLine line : setProperties.getPropertyLines()) { + String type = (line.getPropertyType()==null) ? "" : "{"+line.getPropertyType().name()+"}"; + String values = valuesToString(line.getPropertyValues(), line.getPropertyType()); + if (line.isDefault()) { + formatter.format("default %s%s to %s%n", line.getPropertyName(), type, values); + } else { + formatter.format("set %s%s to %s%n", line.getPropertyName(), type, values); + } + } + formatter.format("end%n"); + } + + private static String valuesToString(@NotNull List<Object> values, @Nullable PropertyLine.PropertyType type) { + List<String> strings = values.stream() + .map(o -> { + if (type == null || type == PropertyLine.PropertyType.String) { + String escapequotes = Objects.toString(o, "").replace("\"", "\\\""); + return "\"" + escapequotes + "\""; + } else if (type == PropertyLine.PropertyType.Date) { + return "\"" + ISO8601.format((Calendar) o) + "\""; + } else { + return Objects.toString(o, null); + } + }) + .collect(Collectors.toList()); + return listToString(strings); + } +} \ No newline at end of file diff --git a/src/main/java/org/apache/sling/feature/cpconverter/repoinit/NoOpVisitor.java b/src/main/java/org/apache/sling/feature/cpconverter/repoinit/NoOpVisitor.java new file mode 100644 index 0000000..6bd558d --- /dev/null +++ b/src/main/java/org/apache/sling/feature/cpconverter/repoinit/NoOpVisitor.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sling.feature.cpconverter.repoinit; + +import org.apache.sling.repoinit.parser.operations.AddGroupMembers; +import org.apache.sling.repoinit.parser.operations.CreateGroup; +import org.apache.sling.repoinit.parser.operations.CreatePath; +import org.apache.sling.repoinit.parser.operations.CreateServiceUser; +import org.apache.sling.repoinit.parser.operations.CreateUser; +import org.apache.sling.repoinit.parser.operations.DeleteGroup; +import org.apache.sling.repoinit.parser.operations.DeleteServiceUser; +import org.apache.sling.repoinit.parser.operations.DeleteUser; +import org.apache.sling.repoinit.parser.operations.DisableServiceUser; +import org.apache.sling.repoinit.parser.operations.OperationVisitor; +import org.apache.sling.repoinit.parser.operations.RegisterNamespace; +import org.apache.sling.repoinit.parser.operations.RegisterNodetypes; +import org.apache.sling.repoinit.parser.operations.RegisterPrivilege; +import org.apache.sling.repoinit.parser.operations.RemoveGroupMembers; +import org.apache.sling.repoinit.parser.operations.SetAclPaths; +import org.apache.sling.repoinit.parser.operations.SetAclPrincipalBased; +import org.apache.sling.repoinit.parser.operations.SetAclPrincipals; +import org.apache.sling.repoinit.parser.operations.SetProperties; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +abstract class NoOpVisitor implements OperationVisitor { + + static String listToString(@NotNull List<String> list) { + if (list.isEmpty()) { + return ""; + } else { + return String.join(",", list); + } + } + + @Override + public void visitCreateGroup(CreateGroup createGroup) { + } + + @Override + public void visitDeleteGroup(DeleteGroup deleteGroup) { + } + + @Override + public void visitCreateUser(CreateUser createUser) { + } + + @Override + public void visitDeleteUser(DeleteUser deleteUser) { + } + + @Override + public void visitCreateServiceUser(CreateServiceUser createServiceUser) { + } + + @Override + public void visitDeleteServiceUser(DeleteServiceUser deleteServiceUser) { + } + + @Override + public void visitSetAclPrincipal(SetAclPrincipals setAclPrincipals) { + } + + @Override + public void visitSetAclPaths(SetAclPaths setAclPaths) { + } + + @Override + public void visitSetAclPrincipalBased(SetAclPrincipalBased setAclPrincipalBased) { + } + + @Override + public void visitCreatePath(CreatePath createPath) { + } + + @Override + public void visitRegisterNamespace(RegisterNamespace registerNamespace) { + } + + @Override + public void visitRegisterNodetypes(RegisterNodetypes registerNodetypes) { + } + + @Override + public void visitRegisterPrivilege(RegisterPrivilege registerPrivilege) { + } + + @Override + public void visitDisableServiceUser(DisableServiceUser disableServiceUser) { + } + + @Override + public void visitAddGroupMembers(AddGroupMembers addGroupMembers) { + } + + @Override + public void visitRemoveGroupMembers(RemoveGroupMembers removeGroupMembers) { + } + + @Override + public void visitSetProperties(SetProperties setProperties) { + } +} \ No newline at end of file diff --git a/src/main/java/org/apache/sling/feature/cpconverter/repoinit/OperationProcessor.java b/src/main/java/org/apache/sling/feature/cpconverter/repoinit/OperationProcessor.java new file mode 100644 index 0000000..d1e319c --- /dev/null +++ b/src/main/java/org/apache/sling/feature/cpconverter/repoinit/OperationProcessor.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sling.feature.cpconverter.repoinit; + +import org.apache.sling.feature.cpconverter.accesscontrol.EnforceInfo; +import org.apache.sling.repoinit.parser.operations.Operation; +import org.apache.sling.repoinit.parser.operations.OperationVisitor; +import org.jetbrains.annotations.NotNull; + +import java.util.Formatter; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +public class OperationProcessor { + + // keep track of system users across different 'scripts' + private final Set<String> systemUserIds = new LinkedHashSet<>(); + + public void apply(@NotNull List<Operation> ops, @NotNull Formatter formatter, @NotNull EnforceInfo enforceInfo) { + ConversionMap toConvert = new ConversionMap(); + + OperationVisitor[] visitors = { + new DefaultVisitor(formatter), + new SystemUserVisitor(formatter, enforceInfo, systemUserIds), + new AccessControlVisitor(formatter, enforceInfo, toConvert, systemUserIds) + }; + + for (Operation op : ops) { + for (OperationVisitor v : visitors) { + op.accept(v); + } + } + + // finally generate repo-init statements for acl-statements that are recorded as to be + // converted to principal-based setup. + toConvert.generateRepoInit(formatter); + } +} \ No newline at end of file diff --git a/src/main/java/org/apache/sling/feature/cpconverter/repoinit/SystemUserVisitor.java b/src/main/java/org/apache/sling/feature/cpconverter/repoinit/SystemUserVisitor.java new file mode 100644 index 0000000..2cad311 --- /dev/null +++ b/src/main/java/org/apache/sling/feature/cpconverter/repoinit/SystemUserVisitor.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sling.feature.cpconverter.repoinit; + +import org.apache.sling.feature.cpconverter.accesscontrol.EnforceInfo; +import org.apache.sling.repoinit.parser.operations.CreateServiceUser; +import org.jetbrains.annotations.NotNull; + +import java.util.Formatter; +import java.util.Set; + +class SystemUserVisitor extends NoOpVisitor { + + private final Formatter formatter; + private final EnforceInfo enforceInfo; + private final Set<String> systemUserIds; + + SystemUserVisitor(@NotNull Formatter formatter, @NotNull EnforceInfo enforceInfo, @NotNull Set<String> systemUserIds) { + this.formatter = formatter; + this.enforceInfo = enforceInfo; + this.systemUserIds = systemUserIds; + } + @Override + public void visitCreateServiceUser(CreateServiceUser createServiceUser) { + String id = createServiceUser.getUsername(); + String path = createServiceUser.getPath(); + systemUserIds.add(id); + + if (enforceInfo.enforcePrincipalBased(id)) { + formatter.format("create service user %s with forced path %s%n", id, enforceInfo.calculateEnforcedIntermediatePath(path)); + } else if (path == null || path.isEmpty()) { + formatter.format("create service user %s%n", id); + } else { + String forced = (createServiceUser.isForcedPath()) ? "forced " : ""; + formatter.format("create service user %s with %spath %s%n", id, forced, path); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/apache/sling/feature/cpconverter/shared/NodeTypeUtil.java b/src/main/java/org/apache/sling/feature/cpconverter/shared/NodeTypeUtil.java new file mode 100644 index 0000000..635ef0f --- /dev/null +++ b/src/main/java/org/apache/sling/feature/cpconverter/shared/NodeTypeUtil.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sling.feature.cpconverter.shared; + +import org.jetbrains.annotations.NotNull; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public final class NodeTypeUtil { + + private NodeTypeUtil() {} + + public static List<String> generateRepoInitLines(@NotNull BufferedReader rawLines) throws IOException { + List<String> lines = new ArrayList<>(); + lines.add("register nodetypes"); + lines.add("<<==="); + + String raw; + while((raw = rawLines.readLine()) != null) { + if (raw.isEmpty()) { + lines.add(""); + } else { + lines.add("<< "+raw); + } + } + lines.add("===>>"); + return lines; + } + +} \ No newline at end of file diff --git a/src/test/java/org/apache/sling/feature/cpconverter/handlers/ConfigurationEntryHandlerTest.java b/src/test/java/org/apache/sling/feature/cpconverter/handlers/ConfigurationEntryHandlerTest.java index 849f259..2b2bac3 100644 --- a/src/test/java/org/apache/sling/feature/cpconverter/handlers/ConfigurationEntryHandlerTest.java +++ b/src/test/java/org/apache/sling/feature/cpconverter/handlers/ConfigurationEntryHandlerTest.java @@ -43,6 +43,7 @@ import org.apache.sling.feature.Extension; import org.apache.sling.feature.Feature; import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter; import org.apache.sling.feature.cpconverter.accesscontrol.AclManager; +import org.apache.sling.feature.cpconverter.accesscontrol.DefaultAclManager; import org.apache.sling.feature.cpconverter.accesscontrol.Mapping; import org.apache.sling.feature.cpconverter.features.DefaultFeaturesManager; import org.apache.sling.feature.cpconverter.features.FeaturesManager; @@ -157,7 +158,7 @@ public class ConfigurationEntryHandlerTest { when(featuresManager.getRunMode(anyString())).thenReturn(feature); ContentPackage2FeatureModelConverter converter = mock(ContentPackage2FeatureModelConverter.class); when(converter.getFeaturesManager()).thenReturn(featuresManager); - AclManager aclManager = mock(AclManager.class); + AclManager aclManager = spy(new DefaultAclManager()); when(converter.getAclManager()).thenReturn(aclManager); configurationEntryHandler.handle(resourceConfiguration, archive, entry, converter); diff --git a/src/test/java/org/apache/sling/feature/cpconverter/handlers/RepoInitTest.java b/src/test/java/org/apache/sling/feature/cpconverter/handlers/RepoInitTest.java new file mode 100644 index 0000000..3138470 --- /dev/null +++ b/src/test/java/org/apache/sling/feature/cpconverter/handlers/RepoInitTest.java @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.sling.feature.cpconverter.handlers; + +import com.google.common.collect.Lists; +import org.apache.jackrabbit.vault.fs.io.Archive; +import org.apache.sling.feature.ArtifactId; +import org.apache.sling.feature.Configuration; +import org.apache.sling.feature.Configurations; +import org.apache.sling.feature.Extension; +import org.apache.sling.feature.Feature; +import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter; +import org.apache.sling.feature.cpconverter.accesscontrol.AclManager; +import org.apache.sling.feature.cpconverter.accesscontrol.DefaultAclManager; +import org.apache.sling.feature.cpconverter.accesscontrol.Mapping; +import org.apache.sling.feature.cpconverter.features.DefaultFeaturesManager; +import org.apache.sling.feature.cpconverter.features.FeaturesManager; +import org.apache.sling.feature.cpconverter.repoinit.OperationProcessor; +import org.apache.sling.feature.io.json.ConfigurationJSONWriter; +import org.apache.sling.repoinit.parser.impl.RepoInitParserService; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Collection; +import java.util.Dictionary; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(Parameterized.class) +public class RepoInitTest { + + private static final String REPOINIT_PID = "org.apache.sling.jcr.repoinit.RepositoryInitializer"; + private static final String PATH_PREFIX = "/jcr_root/apps/asd/config.publish/" + REPOINIT_PID; + + private static final String REPOINIT_CONVERSION_PATH = "/jcr_root/apps/asd/config.publish/" + REPOINIT_PID + "-conversion-test.config"; + + private final AbstractConfigurationEntryHandler configurationEntryHandler; + private final boolean enforcePrincipalBasedAcSetup; + private final String enforcedPath; + + private final String name; + + @Parameterized.Parameters(name = "name={1}") + public static Collection<Object[]> parameters() { + return Lists.newArrayList( + new Object[] { true, "Enforce principal-based ac setup" }, + new Object[] { false, "Don't enforce principal-based ac setup" }); + }; + + public RepoInitTest(boolean enforcePrincipalBasedAcSetup, String name) { + this.configurationEntryHandler = new ConfigurationEntryHandler(); + this.enforcePrincipalBasedAcSetup = enforcePrincipalBasedAcSetup; + this.enforcedPath = (enforcePrincipalBasedAcSetup) ? "/home/users/system/cq:services" : null; + this.name = name; + } + + @Test + public void parseConversionRepoInit() throws Exception { + String path = PATH_PREFIX + "-conversion-test.config"; + String result = PATH_PREFIX + "-conversion-result.config"; + + Extension expectedExtension; + if (enforcePrincipalBasedAcSetup) { + expectedExtension = extractExtentions(result, false, false); + } else { + expectedExtension = extractExtentions(path, false, false); + } + Extension extension = extractExtentions(path, enforcePrincipalBasedAcSetup, false); + assertNotNull(expectedExtension); + assertNotNull(extension); + String txt = extension.getText(); + assertEquals(name, expectedExtension.getText().trim(), txt.trim()); + + // verify that the generated repo-init is valid + assertFalse(name, new RepoInitParserService().parse(new StringReader(txt)).isEmpty()); + } + + @Test + public void parseConversionOmittedForServiceUserRepoInit() throws Exception { + String path = PATH_PREFIX + "-conversion-test.config"; + + Extension expectedExtension = extractExtentions(path, false, true); + Extension extension = extractExtentions(path, enforcePrincipalBasedAcSetup, true); + assertNotNull(expectedExtension); + assertNotNull(extension); + String txt = extension.getText(); + assertEquals(name, expectedExtension.getText().trim(), txt.trim()); + + // verify that the generated repo-init is valid + assertFalse(name, new RepoInitParserService().parse(new StringReader(txt)).isEmpty()); + } + + @Test + public void parseNoConversionRepoInit() throws Exception { + String path = PATH_PREFIX + "-no-conversion-test.config"; + + Extension expectedExtension = extractExtentions(path, false, false); + Extension extension = extractExtentions(path, enforcePrincipalBasedAcSetup, false); + assertNotNull(expectedExtension); + assertNotNull(extension); + String txt = extension.getText(); + assertEquals(name, expectedExtension.getText().trim(), txt.trim()); + + // verify that the generated repo-init is valid + assertFalse(name, new RepoInitParserService().parse(new StringReader(txt)).isEmpty()); + } + + @Test + public void parseNoConversionWithDiffRepoInit() throws Exception { + // NOTE: create-path statements with default primary type and set-property cannot be converted 1:1 + // See SLING-10231, SLING-10238 and FIXMEs in DefaultVisitor + String path = PATH_PREFIX + "-no-conv-with-diff.config"; + + String resultTxt = "create path /test(sling:Folder)/a(nt:folder mixin mix:referenceable,mix:shareable)/b(nt:unstructured)/c(sling:Folder mixin mix:created)\n"+ + "create path /test(sling:Folder)/a(nt:folder mixin mix:referenceable,mix:shareable)/b(nt:unstructured)/c(sling:Folder mixin mix:created)\n"+ + " set properties on /test\n"+ + "set testprop{String} to \"one=two\"\n"+ + "set testprop{String} to \"\\\"one=two\\\"\"\n"+ + "set sling:ResourceType{String} to \"/x/y/z\"\n"+ + "default someInteger{Long} to 42\n"+ + "set someFlag{Boolean} to true\n"+ + "default someDate{Date} to \"2020-03-19T11:39:33.437+05:30\"\n"+ + "set quotedMix{String} to \"quoted\",\"non-quoted\",\"the last \\\" one\"\n"+ + "set aStringMultiValue{String} to \"one\",\"two\",\"three\"\n"+ + "set aLongMultiValue{Long} to 1,2,3\n"+ + "set curlyBracketsAndDoubleQuotes{String} to \"{\\\"one, two\\\":\\\"three, four\\\"}\"\n"+ + "set curlyBracketsAndSingleQuotes{String} to \"{'five, six':'seven,eight'}\"\n"+ + "end"; + Extension extension = extractExtentions(path, enforcePrincipalBasedAcSetup, false); + assertNotNull(extension); + String expectedTxt = (enforcePrincipalBasedAcSetup) ? resultTxt : extractExtentions(path, false, false).getText(); + String txt = extension.getText(); + assertEquals(name, expectedTxt, txt.trim()); + + // verify that the generated repo-init is valid + assertFalse(name, new RepoInitParserService().parse(new StringReader(txt)).isEmpty()); + } + + private Extension extractExtentions(@NotNull String path, boolean enforcePrincipalBasedAcSetup, boolean addMappingById) throws Exception { + Archive archive = mock(Archive.class); + Archive.Entry entry = mock(Archive.Entry.class); + + when(entry.getName()).thenReturn(path.substring(path.lastIndexOf('/') + 1)); + when(archive.openInputStream(entry)).thenReturn(getClass().getResourceAsStream(path.substring(1))); + + Feature feature = new Feature(new ArtifactId("org.apache.sling", "org.apache.sling.cp2fm", "0.0.1", null, null)); + FeaturesManager featuresManager = spy(DefaultFeaturesManager.class); + when(featuresManager.getTargetFeature()).thenReturn(feature); + doCallRealMethod().when(featuresManager).addConfiguration(anyString(), anyString(), anyString(), any()); + when(featuresManager.getRunMode(anyString())).thenReturn(feature); + + AclManager aclManager = spy(new DefaultAclManager((enforcePrincipalBasedAcSetup) ? enforcedPath : null)); + if (addMappingById) { + aclManager.addMapping(new Mapping("org.apache.sling.testbundle:sub1=su1")); + aclManager.addMapping(new Mapping("org.apache.sling.testbundle:sub2=su2")); + aclManager.addMapping(new Mapping("org.apache.sling.testbundle:sub3=su3")); + aclManager.addMapping(new Mapping("org.apache.sling.testbundle=su-second-script")); + } + + ContentPackage2FeatureModelConverter converter = mock(ContentPackage2FeatureModelConverter.class); + when(converter.getFeaturesManager()).thenReturn(featuresManager); + when(converter.getAclManager()).thenReturn(aclManager); + + configurationEntryHandler.handle(path, archive, entry, converter); + return featuresManager.getRunMode("publish").getExtensions().getByName(Extension.EXTENSION_NAME_REPOINIT); + } + +} \ No newline at end of file diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/jcr_root/apps/asd/config.publish/org.apache.sling.jcr.repoinit.RepositoryInitializer-conversion-result.config b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/jcr_root/apps/asd/config.publish/org.apache.sling.jcr.repoinit.RepositoryInitializer-conversion-result.config new file mode 100644 index 0000000..357d76b --- /dev/null +++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/jcr_root/apps/asd/config.publish/org.apache.sling.jcr.repoinit.RepositoryInitializer-conversion-result.config @@ -0,0 +1,45 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with this +# work for additional information regarding copyright ownership. The ASF +# licenses this file to You 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. +scripts=[\ +" +create service user su1 with forced path /home/users/system/cq:services +create service user su2 with forced path /home/users/system/cq:services/myfeature +create service user su3 with forced path /home/users/system/cq:services/myfeature/subtree +set principal ACL for su1 + allow jcr:read,jcr:modifyProperties on /conf,/content restriction(rep:glob,*) + allow jcr:read on /conf,/content restriction(rep:itemNames,jcr:primaryType,jcr:mixinTypes) restriction(rep:ntNames,nt:folder) + remove * on /apps + allow jcr:read,jcr:write on /conf,/libs,:repository,home(su1) +end +set principal ACL for su2 (ACLOptions\=someOption,someOtherOption,namespaced:option) + remove jcr:lockManagement on /content nodetypes nt:folder + remove * on :repository,home(su1) +end +set principal ACL for su2 + allow jcr:read,jcr:write on /conf,/libs,:repository,home(su1) +end +set principal ACL for su3 + deny jcr:versionManagement on /conf,/libs,:repository,home(su1) restriction(rep:glob,/subtree) + remove jcr:modifyProperties on /conf,/libs,:repository,home(su1) +end",\ +" +create service user su-second-script with forced path /home/users/system/cq:services +set principal ACL for su1 + allow jcr:read on /second-script +end +set principal ACL for su-second-script + allow jcr:read on /second-script +end"\ +] \ No newline at end of file diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/jcr_root/apps/asd/config.publish/org.apache.sling.jcr.repoinit.RepositoryInitializer-conversion-test.config b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/jcr_root/apps/asd/config.publish/org.apache.sling.jcr.repoinit.RepositoryInitializer-conversion-test.config new file mode 100644 index 0000000..6d700d4 --- /dev/null +++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/jcr_root/apps/asd/config.publish/org.apache.sling.jcr.repoinit.RepositoryInitializer-conversion-test.config @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with this +# work for additional information regarding copyright ownership. The ASF +# licenses this file to You 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. +scripts=[\ +" +create service user su1 +create service user su2 with path system/myfeature +set ACL for su1 + allow jcr:read,jcr:modifyProperties on /conf,/content restriction(rep:glob,*) + allow jcr:read on /conf,/content restriction(rep:itemNames,jcr:primaryType,jcr:mixinTypes) restriction(rep:ntNames,nt:folder) + remove * on /apps +end +set ACL for su2 (ACLOptions\=someOption,someOtherOption,namespaced:option) + remove jcr:lockManagement on /content nodetypes nt:folder + remove * on :repository,home(su1) +end +create service user su3 with forced path /home/users/system/myfeature/subtree +set ACL on /conf,/libs,:repository,home(su1) + allow jcr:read,jcr:write for su1,su2 + deny jcr:versionManagement for su3 restriction(rep:glob,/subtree) + remove jcr:modifyProperties for su3 +end",\ +" +create service user su-second-script +set ACL on /second-script + allow jcr:read for su1,su-second-script +end" +] \ No newline at end of file diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/jcr_root/apps/asd/config.publish/org.apache.sling.jcr.repoinit.RepositoryInitializer-no-conv-with-diff.config b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/jcr_root/apps/asd/config.publish/org.apache.sling.jcr.repoinit.RepositoryInitializer-no-conv-with-diff.config new file mode 100644 index 0000000..73b51b8 --- /dev/null +++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/jcr_root/apps/asd/config.publish/org.apache.sling.jcr.repoinit.RepositoryInitializer-no-conv-with-diff.config @@ -0,0 +1,34 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with this +# work for additional information regarding copyright ownership. The ASF +# licenses this file to You 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. +scripts=[\ +" +create path /test(sling:Folder)/a(nt:folder mixin mix:referenceable,mix:shareable)/b(nt:unstructured)/c(sling:Folder mixin mix:created) +create path (sling:Folder) /test/a(nt:folder mixin mix:referenceable,mix:shareable)/b(nt:unstructured)/c(mixin mix:created) +",\ +" +set properties on /test + set testprop to \"one\=two\" + set testprop to \"\\\"one\=two\\\"\" + set sling:ResourceType{String} to /x/y/z + default someInteger{Long} to 42 + set someFlag{Boolean} to true + default someDate{Date} to \"2020-03-19T11:39:33.437+05:30\" + set quotedMix to \"quoted\", non-quoted, \"the last \\\" one\" + set aStringMultiValue to \"one\",\"two\",\"three\" + set aLongMultiValue{Long} to 1,2,3 + set curlyBracketsAndDoubleQuotes{String} to \"{\\\"one, two\\\":\\\"three, four\\\"}\" + set curlyBracketsAndSingleQuotes{String} to \"{'five, six':'seven,eight'}\" +end"\ +] \ No newline at end of file diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/jcr_root/apps/asd/config.publish/org.apache.sling.jcr.repoinit.RepositoryInitializer-no-conversion-test.config b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/jcr_root/apps/asd/config.publish/org.apache.sling.jcr.repoinit.RepositoryInitializer-no-conversion-test.config new file mode 100644 index 0000000..e662e39 --- /dev/null +++ b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/jcr_root/apps/asd/config.publish/org.apache.sling.jcr.repoinit.RepositoryInitializer-no-conversion-test.config @@ -0,0 +1,72 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with this +# work for additional information regarding copyright ownership. The ASF +# licenses this file to You 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. +scripts=[\ +" +create service user su with forced path /home/users/system/cq:services/myfeature +set principal ACL for su + allow jcr:all on /var restriction(rep:ntNames,nt:unstructured) +end",\ +" +create group gr1 with path my/group/path +set repository ACL for gr1 + allow jcr:namespaceManagement,jcr:nodeTypeDefinitionManagement +end",\ +" +set ACL on /conf,/content + allow jcr:read,jcr:modifyProperties for gr1 restriction(rep:glob,*) + allow jcr:read for gr1 restriction(rep:itemNames,jcr:primaryType,jcr:mixinTypes) restriction(rep:ntNames\,nt:folder) +end",\ +" +set ACL for gr1 (ACLOptions\=someOption,someOtherOption,namespaced:option) + deny jcr:versionManagement on /content nodetypes nt:folder restriction(rep:glob,/subtree) + remove jcr:lockManagement on /content + remove * on :repository,home(gr1) +end",\ +" +create group gr2 with forced path /home/groups/myfeature +set ACL on /conf,/libs + allow jcr:read,jcr:write for gr1,gr2 restriction(rep:glob,/subtree) + remove jcr:modifyProperties for gr2 +end",\ +" +create group gr3 +create user a +create user b with path myfeature +create user c with forced path /home/users/bla with password plaintext +create user d with password {SHA-256} dc460da4ad72c482231e28e688e01f2778a88ce31a08826899d54ef7183998b5 +add a,b,c,d to group gr3 +remove a,b from group gr1 +disable service user deprecated_service_user : \"Disabled user to make an example\" +delete service user deprecated_service_user +delete user c +delete group gr1",\ +" +create path /test(sling:Folder)/a(nt:folder mixin mix:referenceable,mix:shareable)/b(nt:unstructured)/c(sling:Folder mixin mix:created) +",\ +" +register namespace ( prefix ) http://prefix/v0.0.0",\ +"register nodetypes +<<\=\=\= +<< <slingevent\=\'http://sling.apache.org/jcr/event/1.0\'> +<< [slingevent:Event] > nt:unstructured, nt:hierarchyNode +<< - slingevent:topic (string) +<< - slingevent:properties (binary) +\=\=\=>>",\ +" +register abstract privilege privAbstract +register privilege priv1 +register privilege priv2 with privAbstract,priv1"\ +] +
