| /* |
| * 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.spi.security.authentication.external.impl.principal; |
| |
| import java.security.Principal; |
| import java.text.ParseException; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Enumeration; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| import javax.jcr.PropertyType; |
| import javax.jcr.RepositoryException; |
| import javax.jcr.Value; |
| import javax.jcr.query.Query; |
| |
| import com.google.common.base.Predicates; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Iterators; |
| import com.google.common.collect.Sets; |
| |
| import org.apache.jackrabbit.api.security.principal.GroupPrincipal; |
| import org.apache.jackrabbit.api.security.principal.ItemBasedPrincipal; |
| import org.apache.jackrabbit.api.security.principal.PrincipalManager; |
| import org.apache.jackrabbit.api.security.user.Authorizable; |
| import org.apache.jackrabbit.api.security.user.UserManager; |
| import org.apache.jackrabbit.commons.iterator.AbstractLazyIterator; |
| import org.apache.jackrabbit.oak.api.PropertyState; |
| import org.apache.jackrabbit.oak.api.PropertyValue; |
| import org.apache.jackrabbit.oak.api.QueryEngine; |
| import org.apache.jackrabbit.oak.api.Result; |
| import org.apache.jackrabbit.oak.api.ResultRow; |
| 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.memory.PropertyValues; |
| import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef; |
| import org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncConfig; |
| import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.ExternalIdentityConstants; |
| import org.apache.jackrabbit.oak.spi.security.principal.GroupPrincipals; |
| import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl; |
| import org.apache.jackrabbit.oak.spi.security.principal.PrincipalProvider; |
| import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType; |
| import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration; |
| import org.apache.jackrabbit.oak.spi.security.user.util.UserUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Implementation of the {@code PrincipalProvider} interface that exposes |
| * 'external' principals of type {@link org.apache.jackrabbit.oak.spi.security.principal.GroupPrincipal}. 'External' |
| * refers to the fact that these principals are defined and managed by an |
| * {@link org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProvider}. |
| * |
| * For performance reasons this implementation doesn't lookup principals on the IDP |
| * but relies on a persisted cache inside the repository where the names of these |
| * external principals are synchronized to based on a configurable expiration time. |
| * |
| * Currently, the implementation respects the {@code rep:externalPrincipalNames} |
| * properties, where group membership of external users gets synchronized if |
| * {@link DefaultSyncConfig.User#getDynamicMembership() dynamic membership} has |
| * been enabled. |
| * |
| * Please note that in contrast to the default principal provider implementation |
| * shipped with Oak the group principals known and exposed by this provider are |
| * not backed by an authorizable group and thus cannot be retrieved using |
| * Jackrabbit user management API. |
| * |
| * @since Oak 1.5.3 |
| * @see org.apache.jackrabbit.oak.spi.security.authentication.external.impl.DynamicSyncContext |
| */ |
| class ExternalGroupPrincipalProvider implements PrincipalProvider, ExternalIdentityConstants { |
| |
| private static final Logger log = LoggerFactory.getLogger(ExternalGroupPrincipalProvider.class); |
| |
| private static final String BINDING_PRINCIPAL_NAMES = "principalNames"; |
| |
| private final Root root; |
| private final NamePathMapper namePathMapper; |
| |
| private final UserManager userManager; |
| private final AutoMembershipPrincipals autoMembershipPrincipals; |
| |
| ExternalGroupPrincipalProvider(@NotNull Root root, @NotNull UserConfiguration uc, |
| @NotNull NamePathMapper namePathMapper, |
| @NotNull Map<String, String[]> autoMembershipMapping) { |
| this.root = root; |
| this.namePathMapper = namePathMapper; |
| |
| userManager = uc.getUserManager(root, namePathMapper); |
| autoMembershipPrincipals = new AutoMembershipPrincipals(autoMembershipMapping); |
| } |
| |
| //--------------------------------------------------< PrincipalProvider >--- |
| @Override |
| public Principal getPrincipal(@NotNull String principalName) { |
| Result result = findPrincipals(principalName, true); |
| if (result != null && result.getRows().iterator().hasNext()) { |
| return new ExternalGroupPrincipal(principalName); |
| } else { |
| return null; |
| } |
| } |
| |
| @NotNull |
| @Override |
| public Set<Principal> getMembershipPrincipals(@NotNull Principal principal) { |
| if (!GroupPrincipals.isGroup(principal)) { |
| try { |
| if (principal instanceof ItemBasedPrincipal) { |
| Tree t = root.getTree(((ItemBasedPrincipal) principal).getPath()); |
| return getGroupPrincipals(t); |
| } else { |
| return getGroupPrincipals(userManager.getAuthorizable(principal)); |
| } |
| } catch (RepositoryException e) { |
| log.debug(e.getMessage()); |
| } |
| |
| } |
| return ImmutableSet.of(); |
| } |
| |
| @NotNull |
| @Override |
| public Set<? extends Principal> getPrincipals(@NotNull String userID) { |
| try { |
| return getGroupPrincipals(userManager.getAuthorizable(userID)); |
| } catch (RepositoryException e) { |
| log.debug(e.getMessage()); |
| return ImmutableSet.of(); |
| } |
| } |
| |
| @NotNull |
| @Override |
| public Iterator<? extends Principal> findPrincipals(@Nullable String nameHint, int searchType) { |
| if (PrincipalManager.SEARCH_TYPE_NOT_GROUP != searchType) { |
| Result result = findPrincipals(Strings.nullToEmpty(nameHint), false); |
| if (result != null) { |
| return Iterators.filter(new GroupPrincipalIterator(nameHint, result), Predicates.notNull()); |
| } |
| } |
| |
| return Collections.emptyIterator(); |
| } |
| |
| @NotNull |
| @Override |
| public Iterator<? extends Principal> findPrincipals(int searchType) { |
| return findPrincipals(null, searchType); |
| } |
| |
| //------------------------------------------------------------< private >--- |
| @Nullable |
| private String getIdpName(@NotNull Tree userTree) { |
| PropertyState ps = userTree.getProperty(REP_EXTERNAL_ID); |
| if (ps != null) { |
| return ExternalIdentityRef.fromString(ps.getValue(Type.STRING)).getProviderName(); |
| } else { |
| return null; |
| } |
| } |
| |
| private Set<Principal> getGroupPrincipals(@Nullable Authorizable authorizable) throws RepositoryException { |
| if (authorizable != null && !authorizable.isGroup()) { |
| Tree userTree = root.getTree(authorizable.getPath()); |
| return getGroupPrincipals(userTree); |
| } else { |
| return ImmutableSet.of(); |
| } |
| } |
| |
| private Set<Principal> getGroupPrincipals(@NotNull Tree userTree) { |
| if (userTree.exists() && UserUtil.isType(userTree, AuthorizableType.USER) && userTree.hasProperty(REP_EXTERNAL_PRINCIPAL_NAMES)) { |
| PropertyState ps = userTree.getProperty(REP_EXTERNAL_PRINCIPAL_NAMES); |
| if (ps != null) { |
| // we have an 'external' user that has been synchronized with the dynamic-membership option |
| Set<Principal> groupPrincipals = Sets.newHashSet(); |
| for (String principalName : ps.getValue(Type.STRINGS)) { |
| groupPrincipals.add(new ExternalGroupPrincipal(principalName)); |
| } |
| |
| // add existing group principals as defined with the _autoMembership_ option. |
| groupPrincipals.addAll(autoMembershipPrincipals.get(getIdpName(userTree))); |
| return groupPrincipals; |
| } |
| } |
| // group principals cannot be retrieved |
| return ImmutableSet.of(); |
| } |
| |
| /** |
| * Runs an Oak query searching for {@link #REP_EXTERNAL_PRINCIPAL_NAMES} properties |
| * that match the given name or name hint. |
| * |
| * NOTE: ignore any principals listed in the {@link DefaultSyncConfig.User#autoMembership} |
| * because they are expected to exist in the system and thus will be found |
| * by another principal provider instance. |
| * |
| * @param nameHint The principal name or name hint to be searched for. |
| * @param exactMatch boolean flag indicating if the query should search for |
| * exact matching. |
| * @return The query result. |
| */ |
| @Nullable |
| private Result findPrincipals(@NotNull String nameHint, boolean exactMatch) { |
| try { |
| Map<String, ? extends PropertyValue> bindings = buildBinding(nameHint, exactMatch); |
| String op = (exactMatch) ? " = " : " LIKE "; |
| String statement = "SELECT '" + REP_EXTERNAL_PRINCIPAL_NAMES + "' FROM [rep:User] WHERE PROPERTY([" |
| + REP_EXTERNAL_PRINCIPAL_NAMES + "], '" + PropertyType.TYPENAME_STRING + "')" |
| + op + "$" + BINDING_PRINCIPAL_NAMES + QueryEngine.INTERNAL_SQL2_QUERY; |
| return root.getQueryEngine().executeQuery(statement, Query.JCR_SQL2, bindings, namePathMapper.getSessionLocalMappings()); |
| } catch (ParseException e) { |
| return null; |
| } |
| } |
| |
| /** |
| * Build the map used for the query bindings. |
| * |
| * @param nameHint The name hint |
| * @param exactMatch boolean flag indicating if the query should search for exact matching. |
| * @return the bindings |
| */ |
| @NotNull |
| private static Map<String, ? extends PropertyValue> buildBinding(@NotNull String nameHint, boolean exactMatch) { |
| String val = nameHint; |
| if (!exactMatch) { |
| // not-exact query matching required => add leading and trailing % |
| if (nameHint.isEmpty()) { |
| val = "%"; |
| } else { |
| StringBuilder sb = new StringBuilder(); |
| sb.append('%'); |
| sb.append(nameHint.replace("%", "\\%").replace("_", "\\_")); |
| sb.append('%'); |
| val = sb.toString(); |
| } |
| } |
| return Collections.singletonMap(BINDING_PRINCIPAL_NAMES, PropertyValues.newString(val)); |
| } |
| |
| //------------------------------------------------------< inner classes >--- |
| |
| /** |
| * Implementation of the {@link Group} interface representing external group |
| * identities that are <strong>not</strong> represented as authorizable group |
| * in the repository's user management. |
| */ |
| private final class ExternalGroupPrincipal extends PrincipalImpl implements GroupPrincipal, java.security.acl.Group { |
| |
| private ExternalGroupPrincipal(String principalName) { |
| super(principalName); |
| |
| } |
| |
| @Override |
| public boolean addMember(Principal user) { |
| if (isMember(user)) { |
| return false; |
| } else { |
| throw new UnsupportedOperationException("Adding members to external group principals is not supported."); |
| } |
| } |
| |
| @Override |
| public boolean removeMember(Principal user) { |
| if (!isMember(user)) { |
| return false; |
| } else { |
| throw new UnsupportedOperationException("Removing members from external group principals is not supported."); |
| } |
| } |
| |
| @Override |
| public boolean isMember(Principal member) { |
| if (GroupPrincipals.isGroup(member)) { |
| return false; |
| } |
| try { |
| String name = getName(); |
| if (member instanceof ItemBasedPrincipal) { |
| Tree tree = root.getTree(((ItemBasedPrincipal) member).getPath()); |
| if (UserUtil.isType(tree, AuthorizableType.USER)) { |
| PropertyState ps = tree.getProperty(REP_EXTERNAL_PRINCIPAL_NAMES); |
| return (ps != null && Iterables.contains(ps.getValue(Type.STRINGS), name)); |
| } |
| } else { |
| Authorizable a = userManager.getAuthorizable(member); |
| if (a != null && !a.isGroup()) { |
| Value[] vs = a.getProperty(REP_EXTERNAL_PRINCIPAL_NAMES); |
| if (vs != null) { |
| for (Value v : vs) { |
| if (name.equals(v.getString())) { |
| return true; |
| } |
| } |
| } |
| } |
| } |
| } catch (RepositoryException e) { |
| log.debug(e.getMessage()); |
| } |
| return false; |
| } |
| |
| @Override |
| public Enumeration<? extends Principal> members() { |
| Result result = findPrincipals(getName(), true); |
| if (result != null) { |
| return Iterators.asEnumeration(new MemberIterator(result)); |
| } else { |
| return Iterators.asEnumeration(Collections.<Principal>emptyIterator()); |
| } |
| } |
| } |
| |
| /** |
| * {@link Group} principal iterator converting the query results of |
| * {@link #findPrincipals(String, int)} and {@link #findPrincipals(int)}. |
| * Since each result row provides the values of the {@code PropertyState}, |
| * which matched the query, this iterator needs to filter the individual |
| * property values. |
| * |
| * Additional the iterator keeps track of principal names that have already |
| * been served and will not return duplicates. |
| * |
| * @see #findPrincipals(String, int) |
| * @see #findPrincipals(int) |
| */ |
| private final class GroupPrincipalIterator extends AbstractLazyIterator<Principal> { |
| |
| private final Set<String> processed = new HashSet<String>(); |
| |
| private final String queryString; |
| private final Iterator<? extends ResultRow> rows; |
| |
| private Iterator<String> propValues = Collections.emptyIterator(); |
| |
| private GroupPrincipalIterator(@Nullable String queryString, @NotNull Result queryResult) { |
| this.queryString = queryString; |
| rows = queryResult.getRows().iterator(); |
| } |
| |
| @Override |
| protected Principal getNext() { |
| if (!propValues.hasNext()) { |
| if (rows.hasNext()) { |
| propValues = rows.next().getValue(REP_EXTERNAL_PRINCIPAL_NAMES).getValue(Type.STRINGS).iterator(); |
| } else { |
| propValues = Collections.emptyIterator(); |
| } |
| } |
| while (propValues.hasNext()) { |
| String principalName = propValues.next(); |
| if (principalName != null && !processed.contains(principalName) && matchesQuery(principalName) ) { |
| processed.add(principalName); |
| return new ExternalGroupPrincipal(principalName); |
| } |
| } |
| return null; |
| } |
| |
| private boolean matchesQuery(@NotNull String principalName) { |
| if (queryString == null) { |
| return true; |
| } else { |
| return principalName.contains(queryString); |
| } |
| } |
| } |
| |
| /** |
| * {@code Principal} iterator representing the members of a given |
| * {@link ExternalGroupPrincipal}. The members are collected through an |
| * Oak {@link org.apache.jackrabbit.oak.query.Query Query}. |
| * |
| * Note that the query result is subject to permission evaluation for |
| * the editing {@link Root} based on the accessibility of the individual |
| * {@link #REP_EXTERNAL_PRINCIPAL_NAMES} properties that contain the |
| * exact name of the external group principal. |
| * |
| * @see ExternalGroupPrincipal#members() |
| */ |
| private final class MemberIterator extends AbstractLazyIterator<Principal> { |
| |
| /** |
| * The query results containing the path of the user accounts |
| * (i.e. members) that contain the target group principal in the |
| * {@link #REP_EXTERNAL_PRINCIPAL_NAMES} property values. |
| */ |
| private final Iterator<? extends ResultRow> rows; |
| |
| private MemberIterator(@NotNull Result queryResult) { |
| rows = queryResult.getRows().iterator(); |
| } |
| |
| @Override |
| protected Principal getNext() { |
| while (rows.hasNext()) { |
| String userPath = rows.next().getPath(); |
| try { |
| Authorizable authorizable = userManager.getAuthorizableByPath(userPath); |
| if (authorizable != null) { |
| return authorizable.getPrincipal(); |
| } |
| } catch (RepositoryException e) { |
| log.debug("{}", e.getMessage()); |
| } |
| } |
| return null; |
| } |
| } |
| |
| private final class AutoMembershipPrincipals { |
| |
| private final Map<String, String[]> autoMembershipMapping; |
| private final Map<String, Set<Principal>> principalMap; |
| |
| private AutoMembershipPrincipals(@NotNull Map<String, String[]> autoMembershipMapping) { |
| this.autoMembershipMapping = autoMembershipMapping; |
| this.principalMap = new ConcurrentHashMap<String, Set<Principal>>(autoMembershipMapping.size()); |
| } |
| |
| @NotNull |
| private Collection<Principal> get(@Nullable String idpName) { |
| if (idpName == null) { |
| return ImmutableSet.of(); |
| } |
| |
| Set<Principal> principals; |
| if (!principalMap.containsKey(idpName)) { |
| String[] vs = autoMembershipMapping.get(idpName); |
| if (vs == null) { |
| principals = ImmutableSet.of(); |
| } else { |
| ImmutableSet.Builder<Principal> builder = ImmutableSet.builder(); |
| for (String groupId : autoMembershipMapping.get(idpName)) { |
| try { |
| Authorizable gr = userManager.getAuthorizable(groupId); |
| if (gr != null && gr.isGroup()) { |
| Principal grPrincipal = gr.getPrincipal(); |
| if (GroupPrincipals.isGroup(grPrincipal)) { |
| builder.add(grPrincipal); |
| } else { |
| log.warn("Principal of group {} is not of group type -> Ignoring", groupId); |
| } |
| } else { |
| log.warn("Configured auto-membership group {} does not exist -> Ignoring", groupId); |
| } |
| } catch (RepositoryException e) { |
| log.debug("Failed to retrieved 'auto-membership' group with id {}", groupId, e); |
| } |
| } |
| principals = builder.build(); |
| } |
| principalMap.put(idpName, principals); |
| } else { |
| principals = principalMap.get(idpName); |
| } |
| return principals; |
| } |
| } |
| } |