org.apache.jackrabbit.oak.security.user.UserImporter Maven / Gradle / Ivy
/*
* 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.jackrabbit.oak.security.user;
import java.security.Principal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.PropertyDefinition;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.jackrabbit.api.security.principal.PrincipalIterator;
import org.apache.jackrabbit.api.security.principal.PrincipalManager;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.AuthorizableExistsException;
import org.apache.jackrabbit.api.security.user.Group;
import org.apache.jackrabbit.api.security.user.Impersonation;
import org.apache.jackrabbit.api.security.user.User;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.namepath.NamePathMapper;
import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager;
import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.plugins.value.jcr.PartialValueFactory;
import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
import org.apache.jackrabbit.oak.spi.security.user.util.UserUtil;
import org.apache.jackrabbit.oak.spi.xml.ImportBehavior;
import org.apache.jackrabbit.oak.spi.xml.NodeInfo;
import org.apache.jackrabbit.oak.spi.xml.PropInfo;
import org.apache.jackrabbit.oak.spi.xml.ProtectedNodeImporter;
import org.apache.jackrabbit.oak.spi.xml.ProtectedPropertyImporter;
import org.apache.jackrabbit.oak.spi.xml.ReferenceChangeTracker;
import org.apache.jackrabbit.oak.spi.xml.TextValue;
import org.apache.jackrabbit.oak.plugins.tree.TreeUtil;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.apache.jackrabbit.oak.api.Type.STRINGS;
/**
* {@code UserImporter} implements both {@code ode>ProtectedPropertyImporter}
* and {@code ProtectedNodeImporter} and provides import facilities for protected
* user and group content defined and used by this user management implementation.
*
* The importer is intended to be used by applications that import user content
* extracted from another repository instance and immediately persist the
* imported content using {@link javax.jcr.Session#save()}. Omitting the
* save call will lead to transient, semi-validated user content and eventually
* to inconsistencies.
*
* Note the following restrictions:
*
* - The importer will only be initialized if the user manager exposed by
* the session is an instance of {@code UserManagerImpl}.
*
* - The importer will only be initialized if the editing session starting
* this import is the same as the UserManager's Session instance.
*
* - The jcr:uuid property of user and groups is defined to represent the
* hashed authorizable id as calculated by the UserManager. This importer
* is therefore not able to handle imports with
* {@link ImportUUIDBehavior#IMPORT_UUID_CREATE_NEW}.
* - Importing user/group nodes outside of the hierarchy defined by the two
* configuration options
* {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_GROUP_PATH}
* and {@link org.apache.jackrabbit.oak.spi.security.user.UserConstants#PARAM_USER_PATH}
* will fail upon {@code Root#commit()}. The same may
* be true in case of {@link ImportUUIDBehavior#IMPORT_UUID_COLLISION_REPLACE_EXISTING}
* inserting the user/group node at some other place in the node hierarchy.
* - The same commit hook will make sure that authorizables are never nested
* and are created below a hierarchy of nt:AuthorizableFolder nodes. This isn't
* enforced by means of node type constraints but only by the API. This importer
* itself currently doesn't perform such a validation check.
* - Any attempt to import conflicting data will cause the import to fail
* either immediately or upon calling {@link javax.jcr.Session#save()} with the
* following exceptions:
*
* - {@code rep:members} : Group membership
* - {@code rep:impersonators} : Impersonators of a User.
*
* The import behavior of these two properties is defined by the {@link #PARAM_IMPORT_BEHAVIOR}
* configuration parameter, which can be set to
*
* - {@link ImportBehavior#NAME_IGNORE ignore}: A warning is logged.
* - {@link ImportBehavior#NAME_BESTEFFORT best effort}: A warning is logged
* and the importer tries to fix the problem.
* - {@link ImportBehavior#NAME_ABORT abort}: The import is immediately
* aborted with a ConstraintViolationException. (default)
*
*
*
*/
class UserImporter implements ProtectedPropertyImporter, ProtectedNodeImporter, UserConstants {
private static final Logger log = LoggerFactory.getLogger(UserImporter.class);
private final int importBehavior;
private Root root;
private NamePathMapper namePathMapper;
private ReferenceChangeTracker referenceTracker;
private UserManagerImpl userManager;
private IdentifierManager identifierManager;
private boolean initialized = false;
/**
* Container used to collect group members stored in protected nodes.
*/
private Membership currentMembership;
/**
* map holding the processed memberships. this is needed as both, the property and the node importer, can provide
* memberships during processing. if both would be handled only via the reference tracker {@link Membership#process()}
* would remove the members from the property importer.
*/
private Map memberships = new HashMap<>();
/**
* Temporary store for the pw an imported new user to be able to call
* the creation actions irrespective of the order of protected properties
*/
private String currentPw;
/**
* Remember all new principals for impersonation handling.
*/
private Map principals = new HashMap<>();
UserImporter(ConfigurationParameters config) {
importBehavior = UserUtil.getImportBehavior(config);
}
//----------------------------------------------< ProtectedItemImporter >---
@Override
public boolean init(@NotNull Session session, @NotNull Root root, @NotNull NamePathMapper namePathMapper,
boolean isWorkspaceImport, int uuidBehavior,
@NotNull ReferenceChangeTracker referenceTracker, @NotNull SecurityProvider securityProvider) {
if (!(session instanceof JackrabbitSession)) {
log.debug("Importing protected user content requires a JackrabbitSession");
return false;
}
this.root = root;
this.namePathMapper = namePathMapper;
this.referenceTracker = referenceTracker;
if (initialized) {
throw new IllegalStateException("Already initialized");
}
if (uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW) {
log.debug("ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW isn't supported when importing users or groups.");
return false;
}
if (!canInitUserManager((JackrabbitSession) session, isWorkspaceImport)) {
return false;
}
userManager = new UserManagerImpl(root, new PartialValueFactory(namePathMapper), securityProvider);
initialized = true;
return initialized;
}
private static boolean canInitUserManager(@NotNull JackrabbitSession session, boolean isWorkspaceImport) {
try {
if (!isWorkspaceImport && session.getUserManager().isAutoSave()) {
log.warn("Session import cannot handle user content: UserManager is in autosave mode.");
return false;
}
} catch (RepositoryException e) {
// failed to access user manager or to set the autosave behavior
// -> return false (not initialized) as importer can't operate.
log.error("Failed to initialize UserImporter: ", e);
return false;
}
return true;
}
// -----------------------------------------< ProtectedPropertyImporter >---
@Override
public boolean handlePropInfo(@NotNull Tree parent, @NotNull PropInfo propInfo, @NotNull PropertyDefinition def) throws RepositoryException {
checkInitialized();
if (isPwdNode(parent)) {
// overwrite any properties generated underneath the rep:pwd node
// by "UserManagerImpl#setPassword" by the properties defined by
// the XML to be imported. see OAK-1943 for the corresponding discussion.
return importPwdNodeProperty(parent, propInfo, def);
} else {
Authorizable a = userManager.getAuthorizable(parent);
if (a == null) {
log.debug("Cannot handle protected PropInfo {}. Node {} doesn't represent an Authorizable.", propInfo, parent);
return false;
}
String propName = propInfo.getName();
if (REP_AUTHORIZABLE_ID.equals(propName)) {
if (!isValid(def, NT_REP_AUTHORIZABLE, false)) {
return false;
}
String id = propInfo.getTextValue().getString();
Authorizable existing = userManager.getAuthorizable(id);
if (existing == null) {
String msg = "Cannot handle protected PropInfo " + propInfo + ". Invalid rep:authorizableId.";
log.warn(msg);
throw new ConstraintViolationException(msg);
}
if (a.getPath().equals(existing.getPath())) {
parent.setProperty(REP_AUTHORIZABLE_ID, id);
} else {
throw new AuthorizableExistsException(id);
}
return true;
} else if (REP_PRINCIPAL_NAME.equals(propName)) {
if (!isValid(def, NT_REP_AUTHORIZABLE, false)) {
return false;
}
String principalName = propInfo.getTextValue().getString();
Principal principal = new PrincipalImpl(principalName);
userManager.checkValidPrincipal(principal, a.isGroup());
userManager.setPrincipal(parent, principal);
/*
Remember principal of new user/group for further processing
of impersonators
*/
principals.put(principalName, a.getPrincipal());
return true;
} else if (REP_PASSWORD.equals(propName)) {
if (a.isGroup() || !isValid(def, NT_REP_USER, false)) {
log.warn("Unexpected authorizable or definition for property rep:password");
return false;
}
if (((User) a).isSystemUser()) {
log.warn("System users may not have a password set.");
return false;
}
String pw = propInfo.getTextValue().getString();
userManager.setPassword(parent, a.getID(), pw, true);
currentPw = pw;
return true;
} else if (REP_IMPERSONATORS.equals(propName)) {
if (a.isGroup() || !isValid(def, MIX_REP_IMPERSONATABLE, true)) {
log.warn("Unexpected authorizable or definition for property rep:impersonators");
return false;
}
// since impersonators may be imported later on, postpone processing
// to the end.
// see -> process References
referenceTracker.processedReference(new Impersonators(parent.getPath(), propInfo.getTextValues()));
return true;
} else if (REP_DISABLED.equals(propName)) {
if (a.isGroup() || !isValid(def, NT_REP_USER, false)) {
log.warn("Unexpected authorizable or definition for property rep:disabled");
return false;
}
((User) a).disable(propInfo.getTextValue().getString());
return true;
} else if (REP_MEMBERS.equals(propName)) {
if (!a.isGroup() || !isValid(def, NT_REP_MEMBER_REFERENCES, true)) {
return false;
}
// since group-members are references to user/groups that potentially
// are to be imported later on -> postpone processing to the end.
// see -> process References
getMembership(a.getPath()).addMembers(propInfo.getTextValues());
return true;
} // another protected property -> return false
}
// neither rep:pwd nor authorizable node -> not covered by this importer.
return false;
}
@Override
public void propertiesCompleted(@NotNull Tree protectedParent) throws RepositoryException {
if (isCacheNode(protectedParent)) {
// remove the cache if present
protectedParent.remove();
} else {
Authorizable a = userManager.getAuthorizable(protectedParent);
if (a == null) {
// not an authorizable
return;
}
// make sure the authorizable ID property is always set even if the
// authorizable defined by the imported XML didn't provide rep:authorizableID
if (!protectedParent.hasProperty(REP_AUTHORIZABLE_ID)) {
protectedParent.setProperty(REP_AUTHORIZABLE_ID, a.getID(), Type.STRING);
}
/*
Execute authorizable actions for a NEW user at this point after
having set the password and the principal name (all protected properties
have been processed now).
*/
if (protectedParent.getStatus() == Tree.Status.NEW) {
if (a.isGroup()) {
userManager.onCreate((Group) a);
} else {
userManager.onCreate((User) a, currentPw);
}
}
currentPw = null;
}
}
@Override
public void processReferences() throws RepositoryException {
checkInitialized();
// add all collected memberships to the reference tracker.
for (Membership m: memberships.values()) {
referenceTracker.processedReference(m);
}
memberships.clear();
List