| /* |
| * 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; |
| } |
| |
| } |