blob: b6650859e0fe8af34992feadf14d5ed4fe5bbe89 [file] [log] [blame]
/*
* 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.apache.jackrabbit.spi.Name;
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.util.Text;
import org.apache.jackrabbit.vault.fs.spi.PrivilegeDefinitions;
import org.apache.sling.feature.cpconverter.ConverterException;
import org.apache.sling.feature.cpconverter.features.FeaturesManager;
import org.apache.sling.feature.cpconverter.repoinit.NoOpVisitor;
import org.apache.sling.feature.cpconverter.repoinit.OperationProcessor;
import org.apache.sling.feature.cpconverter.repoinit.createpath.CreatePathSegmentProcessor;
import org.apache.sling.feature.cpconverter.shared.ConverterConstants;
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.CreateServiceUser;
import org.apache.sling.repoinit.parser.operations.DisableServiceUser;
import org.apache.sling.repoinit.parser.operations.Operation;
import org.apache.sling.repoinit.parser.operations.AclLine;
import org.apache.sling.repoinit.parser.operations.CreatePath;
import org.apache.sling.repoinit.parser.operations.RegisterNodetypes;
import org.apache.sling.repoinit.parser.operations.RegisterPrivilege;
import org.apache.sling.repoinit.parser.operations.SetAclPrincipalBased;
import org.apache.sling.repoinit.parser.operations.SetAclPrincipals;
import org.apache.sling.repoinit.parser.operations.WithPathOptions;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.NamespaceException;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Formatter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class DefaultAclManager implements AclManager, EnforceInfo {
private static final Logger log = LoggerFactory.getLogger(DefaultAclManager.class);
private final RepoPath enforcePrincipalBasedSupportedPath;
private final String systemRelPath;
private final boolean alwaysForceSystemUserPath;
private final OperationProcessor processor = new OperationProcessor();
private final Set<SystemUser> systemUsers = new LinkedHashSet<>();
private final Set<SystemUser> globalSystemUsers = new LinkedHashSet<>();
private final Set<String> systemUserIds = new LinkedHashSet<>();
private final Set<Group> groups = new LinkedHashSet<>();
private final Set<User> users = new LinkedHashSet<>();
private final Set<Mapping> mappings = new HashSet<>();
private final Set<String> mappedById = new HashSet<>();
private final Map<String, List<AccessControlEntry>> acls = new HashMap<>();
private final List<RegisterNodetypes> nodetypeOperations = new LinkedList<>();
private volatile PrivilegeDefinitions privilegeDefinitions;
private RepoPath userRootPath;
/**
* Same as {@code DefaultAclManager(null, "system", false)}
* @see ConverterConstants#SYSTEM_USER_REL_PATH_DEFAULT
*/
public DefaultAclManager() {
this(null, ConverterConstants.SYSTEM_USER_REL_PATH_DEFAULT, false);
}
/**
* @param enforcePrincipalBasedSupportedPath The supported path if principal-based access control setup for service users should be enforced; {@code null} otherwise.
* @param systemRelPath The relative intermediate path used for all system users.
* @deprecated Use DefaultAclManager(String,String,boolean) instead
*/
@Deprecated
public DefaultAclManager(@Nullable String enforcePrincipalBasedSupportedPath, @NotNull String systemRelPath) {
this(enforcePrincipalBasedSupportedPath, systemRelPath, false);
log.warn("Deprecated. Please refactor to use DefaultAclManager(String,String,boolean) instead");
}
/**
* Creates a new instance of {@code DefaultAclManager}.
*
* @param enforcePrincipalBasedSupportedPath The supported path if principal-based access control setup for service users should be enforced; {@code null} otherwise.
* @param systemRelPath The relative intermediate path used for all system users.
* @param alwaysForceSystemUserPath Option to make sure all system users are being created with the specified intermediate path (i.e. translating to 'with forced path' statements in repoinit).
*/
public DefaultAclManager(@Nullable String enforcePrincipalBasedSupportedPath, @NotNull String systemRelPath, boolean alwaysForceSystemUserPath) {
if (enforcePrincipalBasedSupportedPath != null && !enforcePrincipalBasedSupportedPath.contains(systemRelPath)) {
throw new RuntimeException("Relative path for system users "+ systemRelPath + " not included in " + enforcePrincipalBasedSupportedPath);
}
this.enforcePrincipalBasedSupportedPath = (enforcePrincipalBasedSupportedPath == null) ? null : new RepoPath(enforcePrincipalBasedSupportedPath);
this.systemRelPath = systemRelPath;
this.alwaysForceSystemUserPath = alwaysForceSystemUserPath;
}
@Override
public boolean addUser(@NotNull User user) {
return users.add(user);
}
@Override
public boolean addGroup(@NotNull Group group) {
return groups.add(group);
}
@Override
public boolean addSystemUser(@NotNull SystemUser systemUser) {
globalSystemUsers.add(systemUser);
if (systemUsers.add(systemUser)) {
recordSystemUserIds(systemUser.getId());
setUserRoot(systemUser.getPath());
return true;
} else {
return false;
}
}
@Override
public void addMapping(@NotNull Mapping mapping) {
if (mappings.add(mapping)) {
for (SystemUser user : globalSystemUsers) {
if (mapping.mapsUser(user.getId())) {
mappedById.add(user.getId());
}
}
}
}
@Override
public boolean addAccessControlEntry(@NotNull String systemUser, @NotNull AccessControlEntry acl) {
if (globalSystemUsers.stream().filter(su -> su.getId().equals(systemUser)).findFirst().isPresent()) {
acls.computeIfAbsent(systemUser, k -> new LinkedList<>()).add(acl);
return true;
}
return false;
}
@Override
public void addRepoinitExtension(@NotNull List<VaultPackageAssembler> packageAssemblers, @NotNull FeaturesManager featureManager)
throws IOException, ConverterException {
try (Formatter formatter = new Formatter()) {
if (privilegeDefinitions != null) {
registerPrivileges(privilegeDefinitions, formatter);
}
for (RegisterNodetypes op : nodetypeOperations) {
formatter.format("%s", op.asRepoInitString());
}
addUsersAndGroups(formatter);
addPaths(formatter, packageAssemblers);
// add the acls
acls.forEach((systemUserID, authorizations) ->
globalSystemUsers.stream().filter(su -> su.getId().equals(systemUserID)).findFirst().ifPresent(systemUser ->
addStatements(systemUser, authorizations, formatter)
));
String text = formatter.toString();
if (!text.isEmpty()) {
featureManager.addOrAppendRepoInitExtension("content-package", text, null);
}
}
}
@Override
public void addRepoinitExtention(@NotNull String source, @Nullable String repoInitText, @Nullable String runMode, @NotNull FeaturesManager featuresManager)
throws IOException, ConverterException {
if (repoInitText == null || repoInitText.trim().isEmpty()) {
return;
}
if ("seed".equalsIgnoreCase(runMode)) {
try {
List<Operation> ops = new RepoInitParserService().parse(new StringReader(repoInitText));
for (Operation op : ops) {
op.accept(new NoOpVisitor() {
@Override
public void visitCreateServiceUser(CreateServiceUser createServiceUser) {
recordSystemUserIds(createServiceUser.getUsername());
}
});
}
} catch (RepoInitParsingException e) {
throw new ConverterException(e.getMessage(), e);
}
return;
}
try (Formatter formatter = new Formatter()) {
if (enforcePrincipalBased() || alwaysForceSystemUserPath) {
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(source, text, runMode);
}
} catch (RepoInitParsingException e) {
throw new ConverterException(e.getMessage(), e);
}
}
private void addUsersAndGroups(@NotNull Formatter formatter) throws ConverterException {
for (SystemUser systemUser : systemUsers) {
// make sure all system users are created first
boolean withForcedPath = (alwaysForceSystemUserPath || enforcePrincipalBased(systemUser));
CreateServiceUser operation = new CreateServiceUser(systemUser.getId(), new WithPathOptions(calculateIntermediatePath(systemUser), withForcedPath));
formatter.format("%s", operation.asRepoInitString());
if (systemUser.getDisabledReason() != null) {
DisableServiceUser disable = new DisableServiceUser(systemUser.getId(), systemUser.getDisabledReason());
disable.setServiceUser(true);
formatter.format("%s", disable.asRepoInitString());
}
if (aclIsBelow(systemUser.getPath())) {
throw new ConverterException("Detected policy on subpath of system-user: " + systemUser);
}
}
// abort the conversion if an access control entry takes effect at or below a user/group which is not
// created by repo-init statements generated here.
for(final Group g : groups) {
if (aclStartsWith(g.getPath())) {
throw new ConverterException("Detected policy on group: " + g);
}
}
for(final User u : users) {
if (aclStartsWith(u.getPath())) {
throw new ConverterException("Detected policy on user: " + u);
}
}
}
@NotNull
private String calculateIntermediatePath(@NotNull SystemUser systemUser) throws ConverterException {
RepoPath intermediatePath = systemUser.getIntermediatePath();
if (enforcePrincipalBased(systemUser)) {
return calculateEnforcedIntermediatePath(intermediatePath.toString());
} else {
return getRelativeIntermediatePath(intermediatePath.toString());
}
}
private void addPaths(@NotNull Formatter formatter, @NotNull List<VaultPackageAssembler> packageAssemblers) {
Set<RepoPath> paths = acls.entrySet().stream()
// filter paths if service user does not exist or will have principal-based ac setup enforced
.filter(entry -> {
Optional<SystemUser> su = getSystemUser(entry.getKey());
return su.isPresent() && !enforcePrincipalBased(su.get());
})
.map(Entry::getValue)
.flatMap(Collection::stream)
// paths only should/need to be create with resource-based access control
.filter(((Predicate<AccessControlEntry>) AccessControlEntry::isPrincipalBased).negate())
.map(AccessControlEntry::getRepositoryPath)
.collect(Collectors.toSet());
paths.stream()
.filter(path -> paths.stream().noneMatch(other -> !other.equals(path) && other.startsWith(path)))
.filter(((Predicate<RepoPath>)RepoPath::isRepositoryPath).negate())
.filter(path -> Stream.of(systemUsers, users, groups).flatMap(Collection::stream)
.noneMatch(user -> user.getPath().startsWith(path)))
.map(path -> getCreatePath(path, packageAssemblers))
.filter(Objects::nonNull)
.forEach(
path -> formatter.format("%s", path.asRepoInitString())
);
}
private boolean aclStartsWith(@NotNull RepoPath path) {
return acls.values().stream().flatMap(List::stream).anyMatch(acl -> acl.getRepositoryPath().startsWith(path));
}
private boolean aclIsBelow(@NotNull RepoPath path) {
return acls.values().stream().flatMap(List::stream).anyMatch(acl -> acl.getRepositoryPath().startsWith(path) && !acl.getRepositoryPath().equals(path));
}
private void addStatements(@NotNull SystemUser systemUser,
@NotNull List<AccessControlEntry> authorizations,
@NotNull Formatter formatter) {
Map<AccessControlEntry, String> resourceEntries = new LinkedHashMap<>();
Map<AccessControlEntry, String> principalEntries = new LinkedHashMap<>();
authorizations.forEach(entry -> {
String path = getRepoInitPath(entry.getRepositoryPath(), systemUser);
if (entry.isPrincipalBased() || enforcePrincipalBased(systemUser)) {
principalEntries.put(entry, path);
} else {
resourceEntries.put(entry, path);
}
});
if (!principalEntries.isEmpty()) {
SetAclPrincipalBased operation = new SetAclPrincipalBased(Collections.singletonList(systemUser.getId()), asAcLines(principalEntries));
formatter.format("%s", operation.asRepoInitString());
}
if (!resourceEntries.isEmpty()) {
SetAclPrincipals operation = new SetAclPrincipals(Collections.singletonList(systemUser.getId()), asAcLines(resourceEntries));
formatter.format("%s", operation.asRepoInitString());
}
}
private static List<AclLine> asAcLines(@NotNull Map<AccessControlEntry, String> entries) {
List<AclLine> lines = new ArrayList<>();
entries.forEach((entry, path) -> lines.add(entry.asAclLine(path)));
return lines;
}
private boolean enforcePrincipalBased() {
return enforcePrincipalBasedSupportedPath != null;
}
private boolean enforcePrincipalBased(@NotNull SystemUser systemUser) {
return enforcePrincipalBased(systemUser.getId());
}
private @NotNull Optional<SystemUser> getSystemUser(@NotNull String id) {
return systemUsers.stream().filter(systemUser -> systemUser.getId().equals(id)).findFirst();
}
@Override
public void addNodetypeRegistration(@NotNull String cndStatements) {
nodetypeOperations.add(new RegisterNodetypes(cndStatements));
}
@Override
public void addPrivilegeDefinitions(@NotNull PrivilegeDefinitions privilegeDefinitions) {
this.privilegeDefinitions = privilegeDefinitions;
}
@Override
public void reset() {
systemUsers.clear();
acls.clear();
nodetypeOperations.clear();
privilegeDefinitions = null;
}
@Override
public void recordSystemUserIds(@NotNull String... systemUserIds) {
for (String id : systemUserIds) {
if (this.systemUserIds.add(id) && mappings.stream().anyMatch(mapping -> mapping.mapsUser(id))) {
mappedById.add(id);
}
}
}
@Override
public boolean enforcePrincipalBased(@NotNull String systemUserId) {
if (enforcePrincipalBased() && systemUserIds.contains(systemUserId)) {
if (mappedById.contains(systemUserId)) {
log.warn("Skip enforcing principal-based access control setup for system user '{}' due to existing mapping by id.", systemUserId);
return false;
} else {
return true;
}
}
return false;
}
@Override
public boolean enforcePath(@NotNull String systemUserId) {
// NOTE: defined by a global configuration option and thus the 'systemUserId' is ignored
return alwaysForceSystemUserPath;
}
@Override
@NotNull
public String calculateEnforcedIntermediatePath(@Nullable String intermediatePath) throws ConverterException {
if (enforcePrincipalBasedSupportedPath == null) {
throw new IllegalStateException("No supported path configured");
}
String supportedPath = getRelativeIntermediatePath(enforcePrincipalBasedSupportedPath.toString());
if (intermediatePath == null || intermediatePath.isEmpty()) {
return supportedPath;
}
String relIntermediate = getRelativeIntermediatePath(intermediatePath);
if (Text.isDescendantOrEqual(supportedPath, relIntermediate)) {
return relIntermediate;
} else if (Text.isDescendant(relIntermediate, supportedPath)) {
return supportedPath;
} else {
String parent = Text.getRelativeParent(relIntermediate, 1);
while (!parent.isEmpty() && !"/".equals(parent)) {
if (Text.isDescendantOrEqual(parent, supportedPath)) {
String relpath = relIntermediate.substring(parent.length());
return supportedPath + relpath;
}
parent = Text.getRelativeParent(parent, 1);
}
throw new ConverterException("Cannot calculate intermediate path for service user. Configured Supported path " +enforcePrincipalBasedSupportedPath+" has no common ancestor with "+intermediatePath);
}
}
@NotNull
private String getRelativeIntermediatePath(@NotNull String intermediatePath) throws ConverterException {
if (intermediatePath.equals(systemRelPath) || intermediatePath.startsWith(systemRelPath+"/")) {
return intermediatePath;
} else {
String p = intermediatePath + "/";
String rel = "/" + systemRelPath + "/";
int i = p.indexOf(rel);
if (i == -1) {
throw new ConverterException("Invalid intermediate path for system user " + intermediatePath + ". Must include "+ systemRelPath);
}
return intermediatePath.substring(i + 1);
}
}
protected @Nullable CreatePath getCreatePath(@NotNull RepoPath path, @NotNull List<VaultPackageAssembler> packageAssemblers) {
if (path.getParent() == null) {
log.debug("Omit create path statement for path '{}'", path);
return null;
}
CreatePath cp = new CreatePath(null);
boolean foundType = CreatePathSegmentProcessor.processSegments(path, packageAssemblers, cp);
if (!foundType && isBelowUserRoot(path)) {
// if no type information has been detected, don't issue a 'create path' statement for nodes below the
// user-root
log.warn("Failed to extract primary type information for node at path '{}'", path);
return null;
} else {
// assume that primary type information is present or can be extracted from default primary type definition
// of the the top-level nodes (i.e. their effective node type definition).
return cp;
}
}
@NotNull
private String getRepoInitPath(@NotNull RepoPath path, @NotNull SystemUser systemUser) {
if (path.isRepositoryPath()) {
return ":repository";
} else if (isHomePath(path, systemUser.getPath())) {
return getHomePath(systemUser);
} else {
AbstractUser other = getOtherUser(path, Stream.of(systemUsers, groups).flatMap(Collection::stream));
if (other != null) {
return getHomePath(other);
}
// not a special path
return path.toString();
}
}
private static boolean isHomePath(@NotNull RepoPath path, @NotNull RepoPath systemUserPath) {
// ACE located in the subtree are not supported
return path.equals(systemUserPath);
}
@Nullable
private static AbstractUser getOtherUser(@NotNull RepoPath path, @NotNull Stream<? extends AbstractUser> abstractUsers) {
return abstractUsers.filter(au -> path.startsWith(au.getPath())).findFirst().orElse(null);
}
@NotNull
private static String getHomePath(@NotNull AbstractUser abstractUser) {
// since ACEs located in the subtree of a user are not supported by the converter,
// there is no need to calculate a potential sub-path to be appended.
return "home("+abstractUser.getId()+")";
}
private static void registerPrivileges(@NotNull PrivilegeDefinitions definitions, @NotNull Formatter formatter) {
NameResolver nameResolver = new DefaultNamePathResolver(definitions.getNamespaceMapping());
for (PrivilegeDefinition privilege : definitions.getDefinitions()) {
try {
RegisterPrivilege operation = new RegisterPrivilege(nameResolver.getJCRName(privilege.getName()), privilege.isAbstract(), getAggregatedNames(privilege, nameResolver));
formatter.format("%s", operation.asRepoInitString());
} catch (NamespaceException e) {
throw new IllegalStateException(e);
}
}
}
@NotNull
private static List<String> getAggregatedNames(@NotNull PrivilegeDefinition definition, @NotNull NameResolver nameResolver) {
Set<Name> aggregatedNames = definition.getDeclaredAggregateNames();
if (aggregatedNames.isEmpty()) {
return Collections.emptyList();
} else {
return aggregatedNames.stream().map(name -> {
try {
return nameResolver.getJCRName(name);
} catch (NamespaceException e) {
throw new IllegalStateException(e);
}
}).collect(Collectors.toList());
}
}
/**
* Record the root path for all users/groups assuming that their common ancestor is a top-level node
*
* @param userPath A user path
*/
private void setUserRoot(@NotNull RepoPath userPath) {
if (userRootPath == null) {
userRootPath = new RepoPath(Text.getAbsoluteParent(userPath.toString(), 0));
}
}
private boolean isBelowUserRoot(@NotNull RepoPath path) {
return userRootPath != null && path.startsWith(userRootPath);
}
}