blob: 2ffae4c4a6cc1c3c1c3bf8696d4462bea9c6a9e7 [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 static org.apache.jackrabbit.oak.spi.security.RegistrationConstants.OAK_SECURITY_NAME;
import java.security.Principal;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Function;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.ObjectArrays;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.apache.jackrabbit.api.security.principal.PrincipalManager;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.commons.PropertiesUtil;
import org.apache.jackrabbit.oak.namepath.NamePathMapper;
import org.apache.jackrabbit.oak.spi.commit.MoveTracker;
import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider;
import org.apache.jackrabbit.oak.spi.lifecycle.RepositoryInitializer;
import org.apache.jackrabbit.oak.spi.security.ConfigurationBase;
import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
import org.apache.jackrabbit.oak.spi.security.SecurityConfiguration;
import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncHandler;
import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.DefaultSyncConfigImpl;
import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.ExternalIdentityConstants;
import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.ExternalLoginModuleFactory;
import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.SyncHandlerMapping;
import org.apache.jackrabbit.oak.spi.security.principal.EmptyPrincipalProvider;
import org.apache.jackrabbit.oak.spi.security.principal.PrincipalConfiguration;
import org.apache.jackrabbit.oak.spi.security.principal.PrincipalManagerImpl;
import org.apache.jackrabbit.oak.spi.security.principal.PrincipalProvider;
import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
import org.apache.jackrabbit.oak.spi.xml.ProtectedItemImporter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of the {@code PrincipalConfiguration} interface that provides
* principal management for {@code Group principals} associated with
* {@link org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentity external identities}
* managed outside of the scope of the repository by an
* {@link org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProvider}.
*
* @since Oak 1.5.3
* @see <a href="https://issues.apache.org/jira/browse/OAK-4101">OAK-4101</a>
*/
@Component(
metatype = true,
label = "Apache Jackrabbit Oak External PrincipalConfiguration",
immediate = true
)
@Service({PrincipalConfiguration.class, SecurityConfiguration.class})
@Properties({
@Property(name = ExternalIdentityConstants.PARAM_PROTECT_EXTERNAL_IDS,
label = "External Identity Protection",
description = "If disabled rep:externalId properties won't be properly protected (backwards compatible behavior). NOTE: for security reasons it is strongly recommend to keep the protection enabled!",
boolValue = ExternalIdentityConstants.DEFAULT_PROTECT_EXTERNAL_IDS),
@Property(name = OAK_SECURITY_NAME,
propertyPrivate= true,
value = "org.apache.jackrabbit.oak.spi.security.authentication.external.impl.principal.ExternalPrincipalConfiguration")
})
public class ExternalPrincipalConfiguration extends ConfigurationBase implements PrincipalConfiguration {
private static final Logger log = LoggerFactory.getLogger(ExternalPrincipalConfiguration.class);
private SyncConfigTracker syncConfigTracker;
private SyncHandlerMappingTracker syncHandlerMappingTracker;
@SuppressWarnings("UnusedDeclaration")
public ExternalPrincipalConfiguration() {
super();
}
public ExternalPrincipalConfiguration(SecurityProvider securityProvider) {
super(securityProvider, securityProvider.getParameters(NAME));
}
//---------------------------------------------< PrincipalConfiguration >---
@NotNull
@Override
public PrincipalManager getPrincipalManager(Root root, NamePathMapper namePathMapper) {
return new PrincipalManagerImpl(getPrincipalProvider(root, namePathMapper));
}
@NotNull
@Override
public PrincipalProvider getPrincipalProvider(Root root, NamePathMapper namePathMapper) {
if (dynamicMembershipEnabled()) {
UserConfiguration uc = getSecurityProvider().getConfiguration(UserConfiguration.class);
return new ExternalGroupPrincipalProvider(root, uc, namePathMapper, syncConfigTracker.getAutoMembership());
} else {
return EmptyPrincipalProvider.INSTANCE;
}
}
//----------------------------------------------< SecurityConfiguration >---
@NotNull
@Override
public String getName() {
return NAME;
}
@NotNull
@Override
public RepositoryInitializer getRepositoryInitializer() {
return new ExternalIdentityRepositoryInitializer(protectedExternalIds());
}
@NotNull
@Override
public List<? extends ValidatorProvider> getValidators(@NotNull String workspaceName, @NotNull Set<Principal> principals, @NotNull MoveTracker moveTracker) {
return ImmutableList.of(new ExternalIdentityValidatorProvider(principals, protectedExternalIds()));
}
@NotNull
@Override
public List<ProtectedItemImporter> getProtectedItemImporters() {
return ImmutableList.<ProtectedItemImporter>of(new ExternalIdentityImporter());
}
//----------------------------------------------------< SCR integration >---
@SuppressWarnings("UnusedDeclaration")
@Activate
private void activate(BundleContext bundleContext, Map<String, Object> properties) {
setParameters(ConfigurationParameters.of(properties));
syncHandlerMappingTracker = new SyncHandlerMappingTracker(bundleContext);
syncHandlerMappingTracker.open();
syncConfigTracker = new SyncConfigTracker(bundleContext, syncHandlerMappingTracker);
syncConfigTracker.open();
}
@SuppressWarnings("UnusedDeclaration")
@Deactivate
private void deactivate() {
if (syncConfigTracker != null) {
syncConfigTracker.close();
}
if (syncHandlerMappingTracker != null) {
syncHandlerMappingTracker.close();
}
}
//------------------------------------------------------------< private >---
private boolean dynamicMembershipEnabled() {
return syncConfigTracker != null && syncConfigTracker.isEnabled;
}
private boolean protectedExternalIds() {
return getParameters().getConfigValue(ExternalIdentityConstants.PARAM_PROTECT_EXTERNAL_IDS, ExternalIdentityConstants.DEFAULT_PROTECT_EXTERNAL_IDS);
}
/**
* {@code ServiceTracker} to detect any {@link SyncHandler} that has
* dynamic membership enabled.
*/
private static final class SyncConfigTracker extends ServiceTracker {
private final SyncHandlerMappingTracker mappingTracker;
private Set<ServiceReference> enablingRefs = new HashSet();
private boolean isEnabled = false;
public SyncConfigTracker(@NotNull BundleContext context, @NotNull SyncHandlerMappingTracker mappingTracker) {
super(context, SyncHandler.class.getName(), null);
this.mappingTracker = mappingTracker;
}
@Override
public Object addingService(ServiceReference reference) {
if (hasDynamicMembership(reference)) {
enablingRefs.add(reference);
isEnabled = true;
}
return super.addingService(reference);
}
@Override
public void modifiedService(ServiceReference reference, Object service) {
if (hasDynamicMembership(reference)) {
enablingRefs.add(reference);
isEnabled = true;
} else {
enablingRefs.remove(reference);
isEnabled = !enablingRefs.isEmpty();
}
super.modifiedService(reference, service);
}
@Override
public void removedService(ServiceReference reference, Object service) {
enablingRefs.remove(reference);
isEnabled = !enablingRefs.isEmpty();
super.removedService(reference, service);
}
private static boolean hasDynamicMembership(ServiceReference reference) {
return PropertiesUtil.toBoolean(reference.getProperty(DefaultSyncConfigImpl.PARAM_USER_DYNAMIC_MEMBERSHIP), DefaultSyncConfigImpl.PARAM_USER_DYNAMIC_MEMBERSHIP_DEFAULT);
}
private Map<String, String[]> getAutoMembership() {
Map<String, String[]> autoMembership = new HashMap();
for (ServiceReference ref : enablingRefs) {
String syncHandlerName = PropertiesUtil.toString(ref.getProperty(DefaultSyncConfigImpl.PARAM_NAME), DefaultSyncConfigImpl.PARAM_NAME_DEFAULT);
String[] userAuthMembership = PropertiesUtil.toStringArray(ref.getProperty(DefaultSyncConfigImpl.PARAM_USER_AUTO_MEMBERSHIP), new String[0]);
String[] groupAuthMembership = PropertiesUtil.toStringArray(ref.getProperty(DefaultSyncConfigImpl.PARAM_GROUP_AUTO_MEMBERSHIP), new String[0]);
String[] membership = ObjectArrays.concat(userAuthMembership, groupAuthMembership, String.class);
for (String idpName : mappingTracker.getIdpNames(syncHandlerName)) {
String[] previous = autoMembership.put(idpName, membership);
if (previous != null) {
String msg = (Arrays.equals(previous, membership)) ? "Duplicate" : "Colliding";
log.debug(msg + " auto-membership configuration for IDP '{}'; replacing previous values {} by {} defined by SyncHandler '{}'",
idpName, Arrays.toString(previous), Arrays.toString(membership), syncHandlerName);
}
}
}
return autoMembership;
}
}
/**
* {@code ServiceTracker} to detect any {@link SyncHandler} that has
* dynamic membership enabled.
*/
private static final class SyncHandlerMappingTracker extends ServiceTracker {
private Map<ServiceReference, String[]> referenceMap = new HashMap<ServiceReference, String[]>();
public SyncHandlerMappingTracker(@NotNull BundleContext context) {
super(context, SyncHandlerMapping.class.getName(), null);
}
@Override
public Object addingService(ServiceReference reference) {
addMapping(reference);
return super.addingService(reference);
}
@Override
public void modifiedService(ServiceReference reference, Object service) {
addMapping(reference);
super.modifiedService(reference, service);
}
@Override
public void removedService(ServiceReference reference, Object service) {
referenceMap.remove(reference);
super.removedService(reference, service);
}
private void addMapping(ServiceReference reference) {
String idpName = PropertiesUtil.toString(reference.getProperty(ExternalLoginModuleFactory.PARAM_IDP_NAME), null);
String syncHandlerName = PropertiesUtil.toString(reference.getProperty(ExternalLoginModuleFactory.PARAM_SYNC_HANDLER_NAME), null);
if (idpName != null && syncHandlerName != null) {
referenceMap.put(reference, new String[]{syncHandlerName, idpName});
} else {
log.warn("Ignoring SyncHandlerMapping with incomplete mapping of IDP '{}' and SyncHandler '{}'", idpName, syncHandlerName);
}
}
private Iterable<String> getIdpNames(@NotNull final String syncHandlerName) {
return Iterables.filter(Iterables.transform(referenceMap.values(), new Function<String[], String>() {
@Nullable
@Override
public String apply(@Nullable String[] input) {
if (input != null && input.length == 2) {
if (syncHandlerName.equals(input[0])) {
return input[1];
} // else: different sync-handler
} else {
log.warn("Unexpected value of reference map. Expected String[] with length = 2");
}
return null;
}
}
), Predicates.notNull());
}
}
}