Return-Path: X-Original-To: apmail-jackrabbit-oak-commits-archive@minotaur.apache.org Delivered-To: apmail-jackrabbit-oak-commits-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 40674DEC0 for ; Fri, 2 Nov 2012 16:16:35 +0000 (UTC) Received: (qmail 44016 invoked by uid 500); 2 Nov 2012 16:16:34 -0000 Delivered-To: apmail-jackrabbit-oak-commits-archive@jackrabbit.apache.org Received: (qmail 43984 invoked by uid 500); 2 Nov 2012 16:16:34 -0000 Mailing-List: contact oak-commits-help@jackrabbit.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: oak-dev@jackrabbit.apache.org Delivered-To: mailing list oak-commits@jackrabbit.apache.org Received: (qmail 43972 invoked by uid 99); 2 Nov 2012 16:16:34 -0000 Received: from nike.apache.org (HELO nike.apache.org) (192.87.106.230) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 02 Nov 2012 16:16:34 +0000 X-ASF-Spam-Status: No, hits=-2000.0 required=5.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Received: from [140.211.11.4] (HELO eris.apache.org) (140.211.11.4) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 02 Nov 2012 16:16:28 +0000 Received: from eris.apache.org (localhost [127.0.0.1]) by eris.apache.org (Postfix) with ESMTP id 7548823888EA; Fri, 2 Nov 2012 16:16:06 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: svn commit: r1405029 - in /jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak: security/user/ spi/security/user/ util/ Date: Fri, 02 Nov 2012 16:16:06 -0000 To: oak-commits@jackrabbit.apache.org From: angela@apache.org X-Mailer: svnmailer-1.0.8-patched Message-Id: <20121102161606.7548823888EA@eris.apache.org> X-Virus-Checked: Checked by ClamAV on apache.org Author: angela Date: Fri Nov 2 16:16:05 2012 New Revision: 1405029 URL: http://svn.apache.org/viewvc?rev=1405029&view=rev Log: OAK-50 : Implement User Management (WIP) Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImporter.java Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/MembershipProvider.java jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserQueryManager.java jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/util/NodeUtil.java Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/MembershipProvider.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/MembershipProvider.java?rev=1405029&r1=1405028&r2=1405029&view=diff ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/MembershipProvider.java (original) +++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/MembershipProvider.java Fri Nov 2 16:16:05 2012 @@ -182,21 +182,24 @@ class MembershipProvider extends Authori } boolean addMember(Tree groupTree, Tree newMemberTree) { + return addMember(groupTree, getContentID(newMemberTree)); + } + + boolean addMember(Tree groupTree, String memberContentId) { if (useMemberNode(groupTree)) { NodeUtil groupNode = new NodeUtil(groupTree); NodeUtil membersNode = groupNode.getOrAddChild(REP_MEMBERS, NT_REP_MEMBERS); // TODO: add implementation throw new UnsupportedOperationException("not implemented: addMember with member-node hierarchy"); } else { - String toAdd = getContentID(newMemberTree); PropertyState property = groupTree.getProperty(REP_MEMBERS); PropertyBuilder propertyBuilder = property == null ? MemoryPropertyBuilder.create(WEAKREFERENCE, REP_MEMBERS) : MemoryPropertyBuilder.create(WEAKREFERENCE, property); - if (propertyBuilder.hasValue(toAdd)) { + if (propertyBuilder.hasValue(memberContentId)) { return false; } else { - propertyBuilder.addValue(toAdd); + propertyBuilder.addValue(memberContentId); } groupTree.setProperty(propertyBuilder.getPropertyState(true)); } @@ -212,6 +215,7 @@ class MembershipProvider extends Authori } } else { String toRemove = getContentID(memberTree); + // FIXME: remove usage of MemoryPropertyBuilder (OAK-372) PropertyState property = groupTree.getProperty(REP_MEMBERS); PropertyBuilder propertyBuilder = property == null ? MemoryPropertyBuilder.create(WEAKREFERENCE, REP_MEMBERS) Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java?rev=1405029&r1=1405028&r2=1405029&view=diff ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java (original) +++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java Fri Nov 2 16:16:05 2012 @@ -30,6 +30,7 @@ import org.apache.jackrabbit.oak.spi.sec import org.apache.jackrabbit.oak.spi.security.SecurityProvider; import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration; import org.apache.jackrabbit.oak.spi.security.user.action.AuthorizableAction; +import org.apache.jackrabbit.oak.spi.xml.ProtectedItemImporter; /** * UserConfigurationImpl... TODO @@ -45,6 +46,7 @@ public class UserConfigurationImpl exten this.securityProvider = securityProvider; } + //----------------------------------------------< SecurityConfiguration >--- @Nonnull @Override public ConfigurationParameters getConfigurationParameters() { @@ -59,6 +61,13 @@ public class UserConfigurationImpl exten @Nonnull @Override + public List getProtectedItemImporters() { + return Collections.singletonList(new UserImporter(config)); + } + + //--------------------------------------------------< UserConfiguration >--- + @Nonnull + @Override public List getAuthorizableActions() { // TODO: create authorizable actions from configuration return Collections.emptyList(); Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImporter.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImporter.java?rev=1405029&view=auto ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImporter.java (added) +++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserImporter.java Fri Nov 2 16:16:05 2012 @@ -0,0 +1,669 @@ +/* + * 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.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.ConstraintViolationException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; + +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.principal.PrincipalIterator; +import org.apache.jackrabbit.api.security.user.Authorizable; +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.api.security.user.UserManager; +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.spi.security.ConfigurationParameters; +import org.apache.jackrabbit.oak.spi.security.principal.TreeBasedPrincipal; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +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.util.NodeUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * {@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)
    • + *
    + *
  • + *
+ */ +public class UserImporter implements ProtectedPropertyImporter, ProtectedNodeImporter { + + private static final Logger log = LoggerFactory.getLogger(UserImporter.class); + + /** + * Parameter name for the import behavior configuration option. + */ + public static final String PARAM_IMPORT_BEHAVIOR = "importBehavior"; + + private final int importBehavior; + + private JackrabbitSession session; + 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; + + /** + * 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 Map currentPw = new HashMap(1); + + UserImporter(ConfigurationParameters config) { + // TODO: review definition of import-behavior configuration + String importBehaviorStr = config.getConfigValue(PARAM_IMPORT_BEHAVIOR, ImportBehavior.nameFromValue(ImportBehavior.IGNORE)); + importBehavior = ImportBehavior.valueFromString(importBehaviorStr); + } + + //----------------------------------------------< ProtectedItemImporter >--- + @Override + public boolean init(Session session, Root root, NamePathMapper namePathMapper, + boolean isWorkspaceImport, int uuidBehavior, + ReferenceChangeTracker referenceTracker) { + + if (!(session instanceof JackrabbitSession)) { + log.debug("Importing protected user content requires a JackrabbitSession"); + return false; + } + + this.session = (JackrabbitSession) session; + 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 (!initUserManager(isWorkspaceImport)) { + return false; + } + + + initialized = true; + return initialized; + } + + private boolean initUserManager(boolean isWorkspaceImport) { + try { + UserManager uMgr = session.getUserManager(); + if (uMgr instanceof UserManagerImpl) { + UserManagerImpl impl = (UserManagerImpl) uMgr; + if (isWorkspaceImport) { + userManager = new UserManagerImpl(null, root, namePathMapper, impl.getSecurityProvider()); + return true; + } else { + if (session != impl.getSession()) { + log.warn("Session import cannot handle user content: different session used by UserManager instance."); + } else if (impl.isAutoSave()) { + log.warn("Session import cannot handle user content: UserManager is in autosave mode."); + } else { + userManager = impl; + return true; + } + } + } else { + log.debug("Failed to initialize UserImporter: Instanceof UserManagerImpl expected."); + } + } 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; + } + + // -----------------------------------------< ProtectedPropertyImporter >--- + @Override + public boolean handlePropInfo(Tree parent, PropInfo propInfo, + PropertyDefinition def) throws RepositoryException { + checkInitialized(); + + Authorizable a = userManager.getAuthorizable(parent); + if (a == null) { + log.warn("Cannot handle protected PropInfo " + propInfo + ". Node " + parent + " doesn't represent a valid Authorizable."); + return false; + } + + String propString = propInfo.getName(); + if (UserConstants.REP_PRINCIPAL_NAME.equals(propString)) { + if (isViolatingDefinition(propString, def, UserConstants.NT_REP_AUTHORIZABLE, false)) { + return false; + } + + String principalName = propInfo.getTextValue().getString(); + userManager.setPrincipal(parent, new TreeBasedPrincipal(principalName, parent, namePathMapper)); + + /* + Execute authorizable actions for a NEW group as this is the + same place in the userManager#createGroup that the actions + are called. + In case of a NEW user the actions are executed if the password + has been imported before. + */ + if (parent.getStatus() == Tree.Status.NEW) { + if (a.isGroup()) { + userManager.onCreate((Group) a); + } else if (currentPw.containsKey(a.getID())) { + userManager.onCreate((User) a, currentPw.remove(a.getID())); + } + } + + return true; + } else if (UserConstants.REP_PASSWORD.equals(propString)) { + if (isWrongType(a, false) || isViolatingDefinition(propString, def, UserConstants.NT_REP_USER, false)) { + return false; + } + + String pw = propInfo.getTextValue().getString(); + userManager.setPassword(parent, pw, false); + + /* + Execute authorizable actions for a NEW user at this point after + having set the password if the principal name has already been + processed, otherwise postpone it. + */ + if (parent.getStatus() == Tree.Status.NEW) { + if (parent.hasProperty(UserConstants.REP_PRINCIPAL_NAME)) { + userManager.onCreate((User) a, pw); + } else { + // principal name not yet available -> remember the pw + currentPw.clear(); + currentPw.put(a.getID(), pw); + } + } + return true; + + } else if (UserConstants.REP_IMPERSONATORS.equals(propString)) { + if (isWrongType(a, false) || isViolatingDefinition(propString, def, UserConstants.MIX_REP_IMPERSONATABLE, true)) { + return false; + } + // since impersonators may be imported later on, postpone processing + // to the end. + // see -> process References + TextValue[] tvs = propInfo.getTextValues(); + referenceTracker.processedReference(new Impersonators(a.getID(), tvs)); + return true; + + } else if (UserConstants.REP_DISABLED.equals(propString)) { + if (isWrongType(a, false) || isViolatingDefinition(propString, def, UserConstants.NT_REP_USER, false)) { + log.warn("Unexpected definition for property rep:disabled"); + return false; + } + + ((User) a).disable(propInfo.getTextValue().getString()); + return true; + + } else if (UserConstants.REP_MEMBERS.equals(propString)) { + if (isWrongType(a, true) || isViolatingDefinition(propString, def, UserConstants.NT_REP_GROUP, 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 + Membership membership = new Membership(a.getID()); + membership.addMembers(propInfo.getTextValues()); + referenceTracker.processedReference(membership); + return true; + + } // else: cannot handle -> return false + + return false; + } + + @Override + public void processReferences() throws RepositoryException { + checkInitialized(); + + List processed = new ArrayList(); + for (Iterator it = referenceTracker.getProcessedReferences(); it.hasNext(); ) { + Object reference = it.next(); + if (reference instanceof Membership) { + ((Membership) reference).process(); + processed.add(reference); + } else if (reference instanceof Impersonators) { + ((Impersonators) reference).process(); + processed.add(reference); + } + } + // successfully processed this entry of the reference tracker + // -> remove from the reference tracker. + referenceTracker.removeReferences(processed); + } + + // ---------------------------------------------< ProtectedNodeImporter >--- + @Override + public boolean start(Tree protectedParent) throws RepositoryException { + String repMembers = namePathMapper.getJcrName(UserConstants.NT_REP_MEMBERS); + NodeUtil parentNode = new NodeUtil(protectedParent); + if (parentNode.hasPrimaryNodeTypeName(repMembers)) { + NodeUtil groupNode = parentNode; + while (!groupNode.isRoot() && groupNode.hasPrimaryNodeTypeName(repMembers)) { + groupNode = groupNode.getParent(); + } + Authorizable auth = userManager.getAuthorizable(groupNode.getTree()); + if (auth == null) { + log.debug("Cannot handle protected node " + protectedParent + ". It nor one of its parents represent a valid Authorizable."); + return false; + } else { + currentMembership = new Membership(auth.getID()); + return true; + } + } // else: parent node is not of type rep:Members + + return false; + } + + @Override + public void startChildInfo(NodeInfo childInfo, List propInfos) throws RepositoryException { + checkNotNull(currentMembership); + + if (UserConstants.NT_REP_MEMBERS.equals(childInfo.getPrimaryTypeName())) { + for (PropInfo prop : propInfos) { + for (TextValue tv : prop.getTextValues()) { + String name = namePathMapper.getJcrName(prop.getName()); + currentMembership.addMember(name, tv.getString()); + } + } + } else { + log.warn("{} is not of type {}", childInfo.getString(), UserConstants.NT_REP_MEMBERS); + } + } + + @Override + public void endChildInfo() throws RepositoryException { + // nothing to do + } + + @Override + public void end(Tree protectedParent) throws RepositoryException { + referenceTracker.processedReference(currentMembership); + currentMembership = null; + } + + //------------------------------------------------------------< private >--- + @Nonnull + private IdentifierManager getIdentifierManager() { + if (identifierManager == null) { + identifierManager = new IdentifierManager(root); + } + return identifierManager; + } + + private void checkInitialized() { + if (!initialized) { + throw new IllegalStateException("Not initialized"); + } + } + + private boolean isNodeType(String oakName, NodeType nodeType) { + return oakName.equals(namePathMapper.getJcrName(nodeType.getName())); + } + + private boolean isViolatingDefinition(String propertyName, PropertyDefinition definition, + String oakNodeTypeName, boolean multipleStatus) { + if (multipleStatus == definition.isMultiple() && isNodeType(oakNodeTypeName, definition.getDeclaringNodeType())) { + return false; + } else { + log.warn("Unexpected definition for property " + propertyName); + return true; + } + } + + private static boolean isWrongType(Authorizable a, boolean isExpectedGroup) { + if (a.isGroup() != isExpectedGroup) { + log.warn("Expected parent node of to be {}.", (isExpectedGroup) ? "group" : "user"); + return false; + } else { + return true; + } + } + + /** + * Handling the import behavior + * + * @param msg + * @throws RepositoryException + * @throws javax.jcr.nodetype.ConstraintViolationException + */ + private void handleFailure(String msg) throws ConstraintViolationException{ + switch(importBehavior){ + case ImportBehavior.IGNORE: + case ImportBehavior.BESTEFFORT: + log.warn(msg); + break; + case ImportBehavior.ABORT: + throw new ConstraintViolationException(msg); + default: + // no other behavior. nothing to do. + + } + } + + //------------------------------------------------------< inner classes >--- + /** + * Inner class used to postpone import of group membership to the very end + * of the import. This allows to import membership of user/groups that + * are only being created during this import. + * + * @see ImportBehavior For additional configuration options. + */ + private final class Membership { + + private final String groupId; + private final List members = new LinkedList(); + + Membership(String groupId) { + this.groupId = groupId; + } + + void addMember(String name, String id) { + members.add(new Member(name, id)); + } + + void addMembers(TextValue[] tvs) { + for (TextValue tv : tvs) { + addMember(null, tv.getString()); + } + } + + void process() throws RepositoryException { + Authorizable a = userManager.getAuthorizable(groupId); + if (a == null || !a.isGroup()) { + throw new RepositoryException(groupId + " does not represent a valid group."); + } + + Group gr = (Group) a; + // 1. collect members to add and to remove. + Map toRemove = new HashMap(); + for (Iterator declMembers = gr.getDeclaredMembers(); declMembers.hasNext(); ) { + Authorizable dm = declMembers.next(); + toRemove.put(dm.getID(), dm); + } + + List toAdd = new ArrayList(); + List nonExisting = new ArrayList(); + + for (Membership.Member memberEntry : members) { + String remapped = referenceTracker.get(memberEntry.contentId); + String memberContentId = (remapped == null) ? memberEntry.contentId : remapped; + + Authorizable member = null; + try { + Tree n = getIdentifierManager().getTree(memberContentId); + member = userManager.getAuthorizable(n); + } catch (RepositoryException e) { + // no such node or failed to retrieve authorizable + // warning is logged below. + } + if (member != null) { + if (toRemove.remove(member.getID()) == null) { + toAdd.add(member); + } // else: no need to remove from rep:members + } else { + handleFailure("New member of " + gr + ": No such authorizable (NodeID = " + memberContentId + ')'); + if (importBehavior == ImportBehavior.BESTEFFORT) { + log.info("ImportBehavior.BESTEFFORT: Remember non-existing member for processing."); + nonExisting.add(memberEntry); + } + } + } + + // 2. adjust members of the group + for (Authorizable m : toRemove.values()) { + if (!gr.removeMember(m)) { + handleFailure("Failed remove existing member (" + m + ") from " + gr); + } + } + for (Authorizable m : toAdd) { + if (!gr.addMember(m)) { + handleFailure("Failed add member (" + m + ") to " + gr); + } + } + + // handling non-existing members in case of best-effort + if (!nonExisting.isEmpty()) { + log.info("ImportBehavior.BESTEFFORT: Found " + nonExisting.size() + " entries of rep:members pointing to non-existing authorizables. Adding to rep:members."); + Tree groupTree = root.getTree(gr.getPath()); + + MembershipProvider membershipProvider = userManager.getMembershipProvider(); + for (Membership.Member member : nonExisting) { + membershipProvider.addMember(groupTree, member.contentId); + } + } + } + + private class Member { + private final String name; + private final String contentId; + + public Member(String name, String contentId) { + super(); + this.name = name; + this.contentId = contentId; + } + } + } + + /** + * Inner class used to postpone import of impersonators to the very end + * of the import. This allows to import impersonation values pointing + * to user that are only being created during this import. + * + * @see ImportBehavior For additional configuration options. + */ + private final class Impersonators { + + private final String userId; + private final Set principalNames = new HashSet(); + + private Impersonators(String userId, TextValue[] values) { + this.userId = userId; + for (TextValue v : values) { + principalNames.add(v.getString()); + } + } + + private void process() throws RepositoryException { + Authorizable a = userManager.getAuthorizable(userId); + if (a == null || a.isGroup()) { + throw new RepositoryException(userId + " does not represent a valid user."); + } + + Impersonation imp = ((User) a).getImpersonation(); + + // 1. collect principals to add and to remove. + Map toRemove = new HashMap(); + for (PrincipalIterator pit = imp.getImpersonators(); pit.hasNext(); ) { + Principal princ = pit.nextPrincipal(); + toRemove.put(princ.getName(), princ); + } + + List toAdd = new ArrayList(); + for (final String principalName : principalNames) { + if (toRemove.remove(principalName) == null) { + // add it to the list of new impersonators to be added. + toAdd.add(new Principal() { + public String getName() { + return principalName; + } + }); + } // else: no need to revoke impersonation for the given principal. + } + + // 2. adjust set of impersonators + boolean bestEffort = false; + for (Principal princ : toRemove.values()) { + if (!imp.revokeImpersonation(princ)) { + handleFailure("Failed to revoke impersonation for " + princ.getName() + " on " + a); + bestEffort = true; + } + } + for (Principal princ : toAdd) { + if (!imp.grantImpersonation(princ)) { + handleFailure("Failed to grant impersonation for " + princ.getName() + " on " + a); + bestEffort = true; + } + } + + if (bestEffort) { + Tree userTree = root.getTree(a.getPath()); + userTree.setProperty(UserConstants.REP_PRINCIPAL_NAME, principalNames, Type.STRINGS); + } + } + } + + /** + * Inner class defining the treatment of membership or impersonator + * values pointing to non-existing authorizables. + */ + public static final class ImportBehavior { + + /** + * If a member or impersonator value cannot be set due to constraints + * enforced by the API implementation, the failure is logged as + * warning but otherwise ignored. + */ + public static final int IGNORE = 1; + /** + * Same as {@link #IGNORE} but in addition tries to circumvent the + * problem. This option should only be used with validated and trusted + * XML passed to the SessionImporter. + */ + public static final int BESTEFFORT = 2; + /** + * Aborts the import as soon as invalid values are detected throwing + * a {@code ConstraintViolationException}. + */ + public static final int ABORT = 3; + + public static final String NAME_IGNORE = "ignore"; + public static final String NAME_BESTEFFORT = "besteffort"; + public static final String NAME_ABORT = "abort"; + + public static int valueFromString(String behaviorString) { + if (NAME_IGNORE.equalsIgnoreCase(behaviorString)) { + return IGNORE; + } else if (NAME_BESTEFFORT.equalsIgnoreCase(behaviorString)) { + return BESTEFFORT; + } else if (NAME_ABORT.equalsIgnoreCase(behaviorString)) { + return ABORT; + } else { + log.error("Invalid behavior " + behaviorString + " -> Using default: ABORT."); + return ABORT; + } + } + + public static String nameFromValue(int importBehavior) { + switch (importBehavior) { + case ImportBehavior.IGNORE: + return NAME_IGNORE; + case ImportBehavior.ABORT: + return NAME_ABORT; + case ImportBehavior.BESTEFFORT: + return NAME_BESTEFFORT; + default: + throw new IllegalArgumentException("Invalid import behavior: " + importBehavior); + } + } + } +} \ No newline at end of file Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java?rev=1405029&r1=1405028&r2=1405029&view=diff ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java (original) +++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java Fri Nov 2 16:16:05 2012 @@ -343,11 +343,21 @@ public class UserManagerImpl implements } @Nonnull + SecurityProvider getSecurityProvider() { + return securityProvider; + } + + @Nonnull ConfigurationParameters getConfig() { return config; } @CheckForNull + Session getSession() { + return session; + } + + @CheckForNull private Authorizable getAuthorizable(String id, Tree tree) throws RepositoryException { if (id == null || tree == null) { return null; @@ -378,7 +388,7 @@ public class UserManagerImpl implements } } - private void setPrincipal(Tree authorizableTree, Principal principal) { + void setPrincipal(Tree authorizableTree, Principal principal) { checkNotNull(principal); authorizableTree.setProperty(UserConstants.REP_PRINCIPAL_NAME, principal.getName()); } @@ -405,7 +415,7 @@ public class UserManagerImpl implements } } - private UserQueryManager getQueryManager() throws RepositoryException { + private UserQueryManager getQueryManager() { if (queryManager == null) { queryManager = new UserQueryManager(this, root); } Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserQueryManager.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserQueryManager.java?rev=1405029&r1=1405028&r2=1405029&view=diff ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserQueryManager.java (original) +++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserQueryManager.java Fri Nov 2 16:16:05 2012 @@ -60,7 +60,7 @@ class UserQueryManager { private final String groupRoot; private final String authorizableRoot; - UserQueryManager(UserManagerImpl userManager, Root root) throws RepositoryException { + UserQueryManager(UserManagerImpl userManager, Root root) { this.userManager = userManager; this.root = root; Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java?rev=1405029&r1=1405028&r2=1405029&view=diff ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java (original) +++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/UserConstants.java Fri Nov 2 16:16:05 2012 @@ -26,6 +26,7 @@ public interface UserConstants { String NT_REP_USER = "rep:User"; String NT_REP_GROUP = "rep:Group"; String NT_REP_MEMBERS = "rep:Members"; + String MIX_REP_IMPERSONATABLE = "rep:Impersonatable"; String REP_PRINCIPAL_NAME = "rep:principalName"; String REP_AUTHORIZABLE_ID = "rep:authorizableId"; String REP_PASSWORD = "rep:password"; Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/util/NodeUtil.java URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/util/NodeUtil.java?rev=1405029&r1=1405028&r2=1405029&view=diff ============================================================================== --- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/util/NodeUtil.java (original) +++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/util/NodeUtil.java Fri Nov 2 16:16:05 2012 @@ -78,10 +78,15 @@ public class NodeUtil { return mapper.getJcrName(tree.getName()); } + @CheckForNull public NodeUtil getParent() { return new NodeUtil(tree.getParent(), mapper); } + public boolean isRoot() { + return tree.isRoot(); + } + public boolean hasChild(String name) { return tree.getChild(name) != null; } @@ -100,6 +105,7 @@ public class NodeUtil { return childUtil; } + @Nonnull public NodeUtil getOrAddChild(String name, String primaryTypeName) { NodeUtil child = getChild(name); return (child != null) ? child : addChild(name, primaryTypeName); @@ -123,6 +129,7 @@ public class NodeUtil { tree.setProperty(name, value); } + @CheckForNull public String getString(String name, String defaultValue) { PropertyState property = tree.getProperty(name); if (property != null && !property.isArray()) { @@ -136,6 +143,7 @@ public class NodeUtil { tree.setProperty(name, value); } + @CheckForNull public String[] getStrings(String name) { PropertyState property = tree.getProperty(name); if (property == null) { @@ -149,10 +157,12 @@ public class NodeUtil { tree.setProperty(name, Arrays.asList(values), STRINGS); } + @CheckForNull public String getName(String name) { return getName(name, null); } + @CheckForNull public String getName(String name, String defaultValue) { PropertyState property = tree.getProperty(name); if (property != null && !property.isArray()) { @@ -167,6 +177,7 @@ public class NodeUtil { tree.setProperty(name, oakName, NAME); } + @CheckForNull public String[] getNames(String name, String... defaultValues) { String[] strings = getStrings(name); if (strings == null) { @@ -201,6 +212,7 @@ public class NodeUtil { tree.setProperty(name, value); } + @Nonnull public List getNodes(String namePrefix) { List nodes = Lists.newArrayList(); for (Tree child : tree.getChildren()) { @@ -224,6 +236,7 @@ public class NodeUtil { tree.setProperty(name, Arrays.asList(values), STRINGS); } + @CheckForNull public Value[] getValues(String name, ValueFactory vf) { PropertyState property = tree.getProperty(name); if (property != null) { @@ -242,6 +255,7 @@ public class NodeUtil { } } + @Nonnull private String getOakName(String jcrName) { String oakName = mapper.getOakName(jcrName); if (oakName == null) {