blob: eb31c37236c9cfab570c8b1ff74069d197e38f53 [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.sling.jcr.resource.internal.helper.jcr;
import static org.apache.sling.api.resource.ResourceResolverFactory.NEW_PASSWORD;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import javax.jcr.Credentials;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.external.URIProvider;
import org.apache.sling.commons.classloader.DynamicClassLoaderManager;
import org.apache.sling.jcr.api.SlingRepository;
import org.apache.sling.jcr.resource.api.JcrResourceConstants;
import org.apache.sling.jcr.resource.internal.HelperData;
import org.apache.sling.spi.resource.provider.ResourceProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JcrProviderStateFactory {
private final Logger logger = LoggerFactory.getLogger(JcrProviderStateFactory.class);
private final ServiceReference<SlingRepository> repositoryReference;
private final SlingRepository repository;
private final AtomicReference<DynamicClassLoaderManager> dynamicClassLoaderManagerReference;
private final AtomicReference<URIProvider[]> uriProviderReference;
public JcrProviderStateFactory(final ServiceReference<SlingRepository> repositoryReference,
final SlingRepository repository,
final AtomicReference<DynamicClassLoaderManager> dynamicClassLoaderManagerReference,
final AtomicReference<URIProvider[]> uriProviderReference) {
this.repository = repository;
this.repositoryReference = repositoryReference;
this.dynamicClassLoaderManagerReference = dynamicClassLoaderManagerReference;
this.uriProviderReference = uriProviderReference;
}
/** Get the calling Bundle from auth info, fail if not provided
* @throws LoginException if no calling bundle info provided
*/
@Nullable
private Bundle extractCallingBundle(@NotNull Map<String, Object> authenticationInfo) throws LoginException {
final Object obj = authenticationInfo.get(ResourceProvider.AUTH_SERVICE_BUNDLE);
if(obj != null && !(obj instanceof Bundle)) {
throw new LoginException("Invalid calling bundle object in authentication info");
}
return (Bundle)obj;
}
@SuppressWarnings("deprecation")
JcrProviderState createProviderState(final @NotNull Map<String, Object> authenticationInfo) throws LoginException {
boolean isLoginAdministrative = Boolean.TRUE.equals(authenticationInfo.get(ResourceProvider.AUTH_ADMIN));
// check whether a session is provided in the authenticationInfo
Session session = getSession(authenticationInfo);
if (session != null && !isLoginAdministrative) {
// by default any session used by the resource resolver returned is
// closed when the resource resolver is closed, except when the session
// was provided in the authenticationInfo
return createJcrProviderState(session, false, authenticationInfo, null);
}
BundleContext bc = null;
try {
final Bundle bundle = extractCallingBundle(authenticationInfo);
if (bundle != null) {
bc = bundle.getBundleContext();
final SlingRepository repo = bc == null ? null : bc.getService(repositoryReference);
if (repo == null) {
logger.warn("Cannot login {} because cannot get SlingRepository on behalf of bundle {} ({})",
isLoginAdministrative ? "admin" : "service",
bundle.getSymbolicName(),
bundle.getBundleId());
throw new LoginException("Repository unavailable");
}
try {
if (isLoginAdministrative) {
session = repo.loginAdministrative(null);
} else {
final Object subService = authenticationInfo.get(ResourceResolverFactory.SUBSERVICE);
final String subServiceName = subService instanceof String ? (String) subService : null;
session = repo.loginService(subServiceName, null);
}
} catch (Throwable t) {
// unget the repository if the service cannot
// login to it, otherwise the repository service
// is let go off when the resource resolver is
// closed and the session logged out
if (session == null) {
bc.ungetService(repositoryReference);
}
throw t;
}
} else if (isLoginAdministrative) {
throw new LoginException("Calling bundle missing in authentication info");
} else {
// requested non-admin session
final Credentials credentials = getCredentials(authenticationInfo);
session = repository.login(credentials, null);
}
} catch (final RepositoryException re) {
throw getLoginException(re);
}
return createJcrProviderState(session, true, authenticationInfo, bc);
}
private JcrProviderState createJcrProviderState(
@NotNull final Session session,
final boolean logoutSession,
@NotNull final Map<String, Object> authenticationInfo,
@Nullable final BundleContext ctx
) throws LoginException {
boolean explicitSessionUsed = (getSession(authenticationInfo) != null);
final Session impersonatedSession = handleImpersonation(session, authenticationInfo, logoutSession, explicitSessionUsed);
if (impersonatedSession != session && explicitSessionUsed) {
// update the session in the auth info map in case the resolver gets cloned in the future
authenticationInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, impersonatedSession);
}
// if we're actually impersonating, we're responsible for closing the session we've created, regardless
// of what the original logoutSession value was.
boolean doLogoutSession = logoutSession || (impersonatedSession != session);
final HelperData data = new HelperData(this.dynamicClassLoaderManagerReference, this.uriProviderReference);
return new JcrProviderState(impersonatedSession, data, doLogoutSession, ctx, ctx == null ? null : repositoryReference);
}
/**
* Handle the sudo if configured. If the authentication info does not
* contain a sudo info, this method simply returns the passed in session. If
* a sudo user info is available, the session is tried to be impersonated.
* The new impersonated session is returned. The original session is closed.
* The session is also closed if the impersonation fails.
*
* @param session
* The session.
* @param authenticationInfo
* The optional authentication info.
* @param logoutSession
* whether to logout the <code>session</code> after impersonation
* or not.
* @param explicitSessionUsed
* whether the JCR session was explicitly given in the auth info or not.
* @return The original session or impersonated session.
* @throws LoginException
* If something goes wrong.
*/
private static Session handleImpersonation(final Session session, final Map<String, Object> authenticationInfo,
final boolean logoutSession, boolean explicitSessionUsed) throws LoginException {
final String sudoUser = getSudoUser(authenticationInfo);
// Do we need session.impersonate() because we are asked to impersonate another user?
boolean needsSudo = (sudoUser != null) && !session.getUserID().equals(sudoUser);
// Do we need session.impersonate() to get an independent copy of the session we were given in the auth info?
boolean needsCloning = !needsSudo && explicitSessionUsed && authenticationInfo.containsKey(ResourceProvider.AUTH_CLONE);
if (!needsSudo && !needsCloning) {
// Nothing to do, but we need to make sure not to enter the try-finally below because it could close the session.
return session;
}
try {
if (needsSudo) {
SimpleCredentials creds = new SimpleCredentials(sudoUser, new char[0]);
copyAttributes(creds, authenticationInfo);
creds.setAttribute(ResourceResolver.USER_IMPERSONATOR, session.getUserID());
return session.impersonate(creds);
} else {
assert needsCloning;
SimpleCredentials creds = new SimpleCredentials(session.getUserID(), new char[0]);
copyAttributes(creds, authenticationInfo);
return session.impersonate(creds);
}
} catch (final RepositoryException re) {
throw getLoginException(re);
} finally {
if (logoutSession) {
session.logout();
}
}
}
/**
* Create a login exception from a repository exception. If the repository
* exception is a {@link javax.jcr.LoginException} a {@link LoginException}
* is created with the same information. Otherwise a {@link LoginException}
* is created which wraps the repository exception.
*
* @param re
* The repository exception.
* @return The login exception.
*/
private static LoginException getLoginException(final RepositoryException re) {
if (re instanceof javax.jcr.LoginException) {
return new LoginException(re.getMessage(), re.getCause());
}
return new LoginException("Unable to login " + re.getMessage(), re);
}
/**
* Create a credentials object from the provided authentication info. If no
* map is provided, <code>null</code> is returned. If a map is provided and
* contains a credentials object, this object is returned. If a map is
* provided but does not contain a credentials object nor a user,
* <code>null</code> is returned. if a map is provided with a user name but
* without a credentials object a new credentials object is created and all
* values from the authentication info are added as attributes.
*
* @param authenticationInfo
* Optional authentication info
* @return A credentials object or <code>null</code>
*/
private static Credentials getCredentials(final Map<String, Object> authenticationInfo) {
Credentials creds = null;
if (authenticationInfo != null) {
final Object credentialsObject = authenticationInfo
.get(JcrResourceConstants.AUTHENTICATION_INFO_CREDENTIALS);
if (credentialsObject instanceof Credentials) {
creds = (Credentials) credentialsObject;
} else {
// otherwise try to create SimpleCredentials if the userId is
// set
final Object userId = authenticationInfo.get(ResourceResolverFactory.USER);
if (userId instanceof String) {
final Object password = authenticationInfo.get(ResourceResolverFactory.PASSWORD);
final SimpleCredentials credentials = new SimpleCredentials((String) userId,
((password instanceof char[]) ? (char[]) password : new char[0]));
// add attributes
copyAttributes(credentials, authenticationInfo);
creds = credentials;
}
}
}
if (creds instanceof SimpleCredentials && authenticationInfo.get(NEW_PASSWORD) instanceof String) {
((SimpleCredentials) creds).setAttribute(NEW_PASSWORD, authenticationInfo.get(NEW_PASSWORD));
}
return creds;
}
/**
* Copies the contents of the source map as attributes into the target
* <code>SimpleCredentials</code> object with the exception of the
* <code>user.jcr.credentials</code> and <code>user.password</code>
* attributes to prevent leaking passwords into the JCR Session attributes
* which might be used for break-in attempts.
*
* @param target
* The <code>SimpleCredentials</code> object whose attributes are
* to be augmented.
* @param source
* The map whose entries (except the ones listed above) are
* copied as credentials attributes.
*/
private static void copyAttributes(final SimpleCredentials target, final Map<String, Object> source) {
final Iterator<Map.Entry<String, Object>> i = source.entrySet().iterator();
while (i.hasNext()) {
final Map.Entry<String, Object> current = i.next();
if (isAttributeVisible(current.getKey())) {
target.setAttribute(current.getKey(), current.getValue());
}
}
}
/**
* Returns <code>true</code> unless the name is
* <code>user.jcr.credentials</code> (
* {@link JcrResourceConstants#AUTHENTICATION_INFO_CREDENTIALS}) or contains
* the string <code>password</code> as in <code>user.password</code> (
* {@link org.apache.sling.api.resource.ResourceResolverFactory#PASSWORD})
*
* @param name
* The name to check whether it is visible or not
* @return <code>true</code> if the name is assumed visible
* @throws NullPointerException
* if <code>name</code> is <code>null</code>
*/
private static boolean isAttributeVisible(final String name) {
return !name.equals(JcrResourceConstants.AUTHENTICATION_INFO_CREDENTIALS) && !name.contains("password");
}
/**
* Return the sudo user information. If the sudo user info is provided, it
* is returned, otherwise <code>null</code> is returned.
*
* @param authenticationInfo
* Authentication info (not {@code null}).
* @return The configured sudo user information or <code>null</code>
*/
private static String getSudoUser(final Map<String, Object> authenticationInfo) {
final Object sudoObject = authenticationInfo.get(ResourceResolverFactory.USER_IMPERSONATION);
if (sudoObject instanceof String) {
return (String) sudoObject;
}
return null;
}
/**
* Returns the session provided as the user.jcr.session property of the
* <code>authenticationInfo</code> map or <code>null</code> if the property
* is not contained in the map or is not a <code>javax.jcr.Session</code>.
*
* @param authenticationInfo
* Authentication info (not {@code null}).
* @return The user.jcr.session property or <code>null</code>
*/
private static Session getSession(final Map<String, Object> authenticationInfo) {
final Object sessionObject = authenticationInfo.get(JcrResourceConstants.AUTHENTICATION_INFO_SESSION);
if (sessionObject instanceof Session) {
return (Session) sessionObject;
}
return null;
}
}