org.apache.jackrabbit.oak.spi.security.authentication.AbstractLoginModule Maven / Gradle / Ivy
/*
* 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;
import java.io.IOException;
import java.security.Principal;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.jcr.Credentials;
import javax.jcr.NoSuchWorkspaceException;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.jackrabbit.oak.api.AuthInfo;
import org.apache.jackrabbit.oak.api.ContentRepository;
import org.apache.jackrabbit.oak.api.ContentSession;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.namepath.NamePathMapper;
import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
import org.apache.jackrabbit.oak.spi.security.authentication.callback.CredentialsCallback;
import org.apache.jackrabbit.oak.spi.security.authentication.callback.PrincipalProviderCallback;
import org.apache.jackrabbit.oak.spi.security.authentication.callback.RepositoryCallback;
import org.apache.jackrabbit.oak.spi.security.authentication.callback.UserManagerCallback;
import org.apache.jackrabbit.oak.spi.security.authentication.callback.WhiteboardCallback;
import org.apache.jackrabbit.oak.spi.security.principal.PrincipalConfiguration;
import org.apache.jackrabbit.oak.spi.security.principal.PrincipalProvider;
import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.osgi.annotation.versioning.ProviderType;
/**
* Abstract implementation of the {@link LoginModule} interface that can act
* as base class for login modules that aim to authenticate subjects against
* information stored in the content repository.
*
*
LoginModule Methods
* This base class provides a simple implementation for the following methods
* of the {@code LoginModule} interface:
*
*
* - {@link LoginModule#initialize(Subject, CallbackHandler, Map, Map) Initialize}:
* Initialization of this abstract module sets the following protected instance
* fields:
*
* - subject: The subject to be authenticated,
* - callbackHandler: The callback handler passed to the login module,
* - shareState: The map used to share state information with other login modules,
* - options: The configuration options of this login module as specified
* in the {@link javax.security.auth.login.Configuration}.
*
*
* - {@link LoginModule#logout() Logout}:
* If the authenticated subject is not empty this logout implementation
* attempts to clear both principals and public credentials and returns
* {@code true}.
* - {@link LoginModule#abort() Abort}: Clears the state of this login
* module by setting all private instance variables created in phase 1 or 2
* to {@code null}. Subclasses are in charge of releasing their own state
* information by either overriding {@link #clearState()}.
*
*
*
Utility Methods
* The following methods are provided in addition:
*
*
* - {@link #clearState()}: Clears all private state information that has
* be created during login. This method in called in {@link #abort()} and
* subclasses are expected to override this method.
* - {@link #getSupportedCredentials()}: Abstract method used by
* {@link #getCredentials()} that reveals which credential implementations
* are supported by the {@code LoginModule}.
* - {@link #getCredentials()}: Tries to retrieve valid (supported)
* Credentials in the following order:
*
* - using a {@link CredentialsCallback},
* - looking for a {@link #SHARED_KEY_CREDENTIALS} entry in the shared
* state (see also {@link #getSharedCredentials()} and finally by
* - searching for valid credentials in the subject.
*
* - {@link #getSharedCredentials()}: This method returns credentials
* passed to the login module with the share state. The key to share credentials
* with a another module extending from this base class is
* {@link #SHARED_KEY_CREDENTIALS}. Note, that this method does not verify
* if the credentials provided by the shared state are
* {@link #getSupportedCredentials() supported}.
* - {@link #getSharedLoginName()}: If the shared state contains an entry
* for {@link #SHARED_KEY_LOGIN_NAME} this method returns the value as login name.
* - {@link #getSecurityProvider()}: Returns the configured security
* provider or {@code null}.
* - {@link #getRoot()}: Provides access to the latest state of the
* repository in order to retrieve user or principal information required to
* authenticate the subject as well as to write back information during
* {@link #commit()}.
* - {@link #getUserManager()}: Returns an instance of the configured
* {@link UserManager} or {@code null}.
* - {@link #getPrincipalProvider()}: Returns an instance of the configured
* principal provider or {@code null}.
* - {@link #getPrincipals(String)}: Utility that returns all principals
* associated with a given user id. This method might be be called after
* successful authentication in order to be able to populate the subject
* during {@link #commit()}. The implementation is a shortcut for calling
* {@link PrincipalProvider#getPrincipals(String) getPrincipals(String userId}
* on the provider exposed by {@link #getPrincipalProvider()}
*
*/
@ProviderType
public abstract class AbstractLoginModule implements LoginModule {
/**
* logger instance
*/
private static final Logger log = LoggerFactory.getLogger(AbstractLoginModule.class);
/**
* Key of the sharedState entry referring to validated Credentials that is
* shared between multiple login modules.
*/
public static final String SHARED_KEY_CREDENTIALS = "org.apache.jackrabbit.credentials";
/**
* Key of the sharedState entry referring to a valid login ID that is shared
* between multiple login modules.
*/
public static final String SHARED_KEY_LOGIN_NAME = "javax.security.auth.login.name";
/**
* Key of the sharedState entry referring to public attributes that are shared
* between multiple login modules.
*/
public static final String SHARED_KEY_ATTRIBUTES = "javax.security.auth.login.attributes";
/**
* Key of the sharedState entry referring to pre authenticated login information that is shared
* between multiple login modules.
*/
public static final String SHARED_KEY_PRE_AUTH_LOGIN = PreAuthenticatedLogin.class.getName();
protected Subject subject;
protected CallbackHandler callbackHandler;
protected Map sharedState;
protected ConfigurationParameters options;
private SecurityProvider securityProvider;
private Whiteboard whiteboard;
private ContentSession systemSession;
private Root root;
//--------------------------------------------------------< LoginModule >---
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
this.sharedState = sharedState;
this.options = (options == null) ? ConfigurationParameters.EMPTY : ConfigurationParameters.of(options);
}
@Override
public boolean logout() throws LoginException {
boolean success = false;
if (!subject.getPrincipals().isEmpty() && !subject.getPublicCredentials(Credentials.class).isEmpty()) {
// clear subject if not readonly
if (!subject.isReadOnly()) {
subject.getPrincipals().clear();
subject.getPublicCredentials().clear();
}
success = true;
}
return success;
}
@Override
public boolean abort() throws LoginException {
clearState();
return true;
}
//--------------------------------------------------------------------------
/**
* Clear state information that has been created during {@link #login()}.
*/
protected void clearState() {
securityProvider = null;
root = null;
if (systemSession != null) {
try {
systemSession.close();
} catch (IOException e) {
log.debug(e.getMessage());
}
}
}
/**
* @return A set of supported credential classes.
*/
@Nonnull
protected abstract Set getSupportedCredentials();
/**
* Tries to retrieve valid (supported) Credentials:
*
* - using a {@link CredentialsCallback},
* - looking for a {@link #SHARED_KEY_CREDENTIALS} entry in the
* shared state (see also {@link #getSharedCredentials()} and finally by
* - searching for valid credentials in the subject.
*
*
* @return Valid (supported) credentials or {@code null}.
*/
@CheckForNull
protected Credentials getCredentials() {
Set supported = getSupportedCredentials();
if (callbackHandler != null) {
log.debug("Login: retrieving Credentials using callback.");
try {
CredentialsCallback callback = new CredentialsCallback();
callbackHandler.handle(new Callback[]{callback});
Credentials creds = callback.getCredentials();
if (creds != null && supported.contains(creds.getClass())) {
log.debug("Login: Credentials '{}' obtained from callback", creds);
return creds;
} else {
log.debug("Login: No supported credentials obtained from callback; trying shared state.");
}
} catch (UnsupportedCallbackException e) {
log.warn(e.getMessage());
} catch (IOException e) {
log.error(e.getMessage());
}
}
Credentials creds = getSharedCredentials();
if (creds != null && supported.contains(creds.getClass())) {
log.debug("Login: Credentials obtained from shared state.");
return creds;
} else {
log.debug("Login: No supported credentials found in shared state; looking for credentials in subject.");
for (Class clz : getSupportedCredentials()) {
Set cds = subject.getPublicCredentials(clz);
if (!cds.isEmpty()) {
log.debug("Login: Credentials found in subject.");
return cds.iterator().next();
}
}
}
log.debug("No credentials found.");
return null;
}
/**
* @return The credentials passed to this login module with the shared state.
* @see #SHARED_KEY_CREDENTIALS
*/
@CheckForNull
protected Credentials getSharedCredentials() {
Credentials shared = null;
if (sharedState.containsKey(SHARED_KEY_CREDENTIALS)) {
Object sc = sharedState.get(SHARED_KEY_CREDENTIALS);
if (sc instanceof Credentials) {
shared = (Credentials) sc;
} else {
log.debug("Login: Invalid value for share state entry " + SHARED_KEY_CREDENTIALS + ". Credentials expected.");
}
}
return shared;
}
/**
* @return The login name passed to this login module with the shared state.
* @see #SHARED_KEY_LOGIN_NAME
*/
@CheckForNull
protected String getSharedLoginName() {
if (sharedState.containsKey(SHARED_KEY_LOGIN_NAME)) {
return sharedState.get(SHARED_KEY_LOGIN_NAME).toString();
} else {
return null;
}
}
/**
* @return The pre authenticated login or {@code null}
* @see #SHARED_KEY_PRE_AUTH_LOGIN
*/
@CheckForNull
protected PreAuthenticatedLogin getSharedPreAuthLogin() {
Object login = sharedState.get(SHARED_KEY_PRE_AUTH_LOGIN);
if (login instanceof PreAuthenticatedLogin) {
return (PreAuthenticatedLogin) login;
} else {
return null;
}
}
/**
* Tries to obtain the {@code SecurityProvider} object from the callback
* handler using a new SecurityProviderCallback and keeps the value as
* private field. If the callback handler isn't able to handle the
* SecurityProviderCallback this method returns {@code null}.
*
* @return The {@code SecurityProvider} associated with this
* {@code LoginModule} or {@code null}.
*/
@CheckForNull
protected SecurityProvider getSecurityProvider() {
if (securityProvider == null && callbackHandler != null) {
RepositoryCallback rcb = new RepositoryCallback();
try {
callbackHandler.handle(new Callback[]{rcb});
securityProvider = rcb.getSecurityProvider();
} catch (Exception e) {
log.debug("Unable to retrieve the SecurityProvider via callback", e);
}
}
return securityProvider;
}
/**
* Tries to obtain the {@code Whiteboard} object from the callback
* handler using a new WhiteboardCallback and keeps the value as
* private field. If the callback handler isn't able to handle the
* WhiteboardCallback this method returns {@code null}.
*
* @return The {@code Whiteboard} associated with this
* {@code LoginModule} or {@code null}.
*/
@CheckForNull
protected Whiteboard getWhiteboard() {
if (whiteboard == null && callbackHandler != null) {
WhiteboardCallback cb = new WhiteboardCallback();
try {
callbackHandler.handle(new Callback[]{cb});
whiteboard = cb.getWhiteboard();
} catch (Exception e) {
log.debug("Unable to retrieve the Whiteboard via callback", e);
}
}
return whiteboard;
}
/**
* Tries to obtain a {@code Root} object from the callback handler using
* a new RepositoryCallback and keeps the value as private field.
* If the callback handler isn't able to handle the RepositoryCallback
* this method returns {@code null}.
*
* @return The {@code Root} associated with this {@code LoginModule} or
* {@code null}.
*/
@CheckForNull
protected Root getRoot() {
if (root == null && callbackHandler != null) {
try {
final RepositoryCallback rcb = new RepositoryCallback();
callbackHandler.handle(new Callback[]{rcb});
final ContentRepository repository = rcb.getContentRepository();
if (repository != null) {
systemSession = Subject.doAs(SystemSubject.INSTANCE, new PrivilegedExceptionAction() {
@Override
public ContentSession run() throws LoginException, NoSuchWorkspaceException {
return repository.login(null, rcb.getWorkspaceName());
}
});
root = systemSession.getLatestRoot();
} else {
log.debug("Unable to retrieve the Root via RepositoryCallback; ContentRepository not available.");
}
} catch (UnsupportedCallbackException | PrivilegedActionException | IOException e) {
log.debug(e.getMessage());
}
}
return root;
}
/**
* Retrieves the {@link UserManager} that should be used to handle
* this authentication. If no user manager has been configure this
* method returns {@code null}.
*
* @return A instance of {@code UserManager} or {@code null}.
*/
@CheckForNull
protected UserManager getUserManager() {
UserManager userManager = null;
SecurityProvider sp = getSecurityProvider();
Root r = getRoot();
if (r != null && sp != null) {
UserConfiguration uc = securityProvider.getConfiguration(UserConfiguration.class);
userManager = uc.getUserManager(r, NamePathMapper.DEFAULT);
}
if (userManager == null && callbackHandler != null) {
try {
UserManagerCallback userCallBack = new UserManagerCallback();
callbackHandler.handle(new Callback[]{userCallBack});
userManager = userCallBack.getUserManager();
} catch (IOException | UnsupportedCallbackException e) {
log.debug(e.getMessage());
}
}
return userManager;
}
/**
* Retrieves the {@link PrincipalProvider} that should be used to handle
* this authentication. If no principal provider has been configure this
* method returns {@code null}.
*
* @return A instance of {@code PrincipalProvider} or {@code null}.
*/
@CheckForNull
protected PrincipalProvider getPrincipalProvider() {
PrincipalProvider principalProvider = null;
SecurityProvider sp = getSecurityProvider();
Root r = getRoot();
if (r != null && sp != null) {
PrincipalConfiguration pc = sp.getConfiguration(PrincipalConfiguration.class);
principalProvider = pc.getPrincipalProvider(r, NamePathMapper.DEFAULT);
}
if (principalProvider == null && callbackHandler != null) {
try {
PrincipalProviderCallback principalCallBack = new PrincipalProviderCallback();
callbackHandler.handle(new Callback[]{principalCallBack});
principalProvider = principalCallBack.getPrincipalProvider();
} catch (IOException | UnsupportedCallbackException e) {
log.debug(e.getMessage());
}
}
return principalProvider;
}
/**
* Retrieves all principals associated with the specified {@code userId} for
* the configured principal provider.
*
* @param userId The id of the user.
* @return The set of principals associated with the given {@code userId}.
* @see #getPrincipalProvider()
*/
@Nonnull
protected Set getPrincipals(@Nonnull String userId) {
PrincipalProvider principalProvider = getPrincipalProvider();
if (principalProvider == null) {
log.debug("Cannot retrieve principals. No principal provider configured.");
return Collections.emptySet();
} else {
return principalProvider.getPrincipals(userId);
}
}
@Nonnull
protected Set getPrincipals(@Nonnull Principal userPrincipal) {
PrincipalProvider principalProvider = getPrincipalProvider();
if (principalProvider == null) {
log.debug("Cannot retrieve principals. No principal provider configured.");
return Collections.emptySet();
} else {
Set principals = new HashSet();
principals.add(userPrincipal);
principals.addAll(principalProvider.getGroupMembership(userPrincipal));
return principals;
}
}
protected static void setAuthInfo(@Nonnull AuthInfo authInfo, @Nonnull Subject subject) {
Set ais = subject.getPublicCredentials(AuthInfo.class);
if (!ais.isEmpty()) {
subject.getPublicCredentials().removeAll(ais);
}
subject.getPublicCredentials().add(authInfo);
}
}