blob: 3ddaaa332b49b33fade34cc26bacd1a0e4fdd59c [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.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;
}
}
}