Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #5486 PropertyFileLoginModule retains PropertyUserStores #5518

Merged
merged 3 commits into from Nov 11, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
134 changes: 50 additions & 84 deletions jetty-jaas/src/main/java/org/eclipse/jetty/jaas/JAASLoginService.java
Expand Up @@ -20,15 +20,16 @@

import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.FailedLoginException;
Expand All @@ -37,17 +38,14 @@
import javax.servlet.ServletRequest;

import org.eclipse.jetty.jaas.callback.DefaultCallbackHandler;
import org.eclipse.jetty.jaas.callback.ObjectCallback;
import org.eclipse.jetty.jaas.callback.RequestParameterCallback;
import org.eclipse.jetty.jaas.callback.ServletRequestCallback;
import org.eclipse.jetty.security.DefaultIdentityService;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.util.ArrayUtil;
import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

Expand All @@ -58,13 +56,14 @@
* Implementation of jetty's LoginService that works with JAAS for
* authorization and authentication.
*/
public class JAASLoginService extends AbstractLifeCycle implements LoginService
public class JAASLoginService extends ContainerLifeCycle implements LoginService
{
private static final Logger LOG = Log.getLogger(JAASLoginService.class);

public static final String DEFAULT_ROLE_CLASS_NAME = "org.eclipse.jetty.jaas.JAASRole";
public static final String[] DEFAULT_ROLE_CLASS_NAMES = {DEFAULT_ROLE_CLASS_NAME};

public static final ThreadLocal<JAASLoginService> INSTANCE = new ThreadLocal<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like an unusual way to share the LoginService with the LoginModule.
Would it be better to do this with a Callback instead? similar how we would use ServletRequestCallback.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that we allow the user to supply their own CallbackHandler class, which might not know anything about the special jetty Callbacks, therefore couldn't supply a value for the JAASLoginService.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that seems strange to me, how would you ever use ServletRequestCallback or RequestParameterCallback inside a login module because they only ever get set on the DefaultCallbackHandler? Seems like using this would also just break if they supplied a custom CallbackHandler.


protected String[] _roleClassNames = DEFAULT_ROLE_CLASS_NAMES;
protected String _callbackHandlerClass;
protected String _realmName;
Expand Down Expand Up @@ -183,6 +182,7 @@ protected void doStart() throws Exception
{
if (_identityService == null)
_identityService = new DefaultIdentityService();
addBean(new PropertyUserStoreManager());
super.doStart();
}

Expand All @@ -193,59 +193,27 @@ public UserIdentity login(final String username, final Object credentials, final
{
CallbackHandler callbackHandler = null;
if (_callbackHandlerClass == null)
{
callbackHandler = new CallbackHandler()
{
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException
{
for (Callback callback : callbacks)
{
if (callback instanceof NameCallback)
{
((NameCallback)callback).setName(username);
}
else if (callback instanceof PasswordCallback)
{
((PasswordCallback)callback).setPassword(credentials.toString().toCharArray());
}
else if (callback instanceof ObjectCallback)
{
((ObjectCallback)callback).setObject(credentials);
}
else if (callback instanceof RequestParameterCallback)
{
RequestParameterCallback rpc = (RequestParameterCallback)callback;
if (request != null)
rpc.setParameterValues(Arrays.asList(request.getParameterValues(rpc.getParameterName())));
}
else if (callback instanceof ServletRequestCallback)
{
((ServletRequestCallback)callback).setRequest(request);
}
else
throw new UnsupportedCallbackException(callback);
}
}
};
}
callbackHandler = new DefaultCallbackHandler();
else
{
Class<?> clazz = Loader.loadClass(_callbackHandlerClass);
callbackHandler = (CallbackHandler)clazz.getDeclaredConstructor().newInstance();
if (DefaultCallbackHandler.class.isAssignableFrom(clazz))
{
DefaultCallbackHandler dch = (DefaultCallbackHandler)callbackHandler;
if (request instanceof Request)
dch.setRequest((Request)request);
dch.setCredential(credentials);
dch.setUserName(username);
}
}

if (callbackHandler instanceof DefaultCallbackHandler)
{
DefaultCallbackHandler dch = (DefaultCallbackHandler)callbackHandler;
if (request instanceof Request)
dch.setRequest((Request)request);
dch.setCredential(credentials);
dch.setUserName(username);
}

//set up the login context
Subject subject = new Subject();
LoginContext loginContext = (_configuration == null ? new LoginContext(_loginModuleName, subject, callbackHandler)
INSTANCE.set(this);
LoginContext loginContext =
(_configuration == null ? new LoginContext(_loginModuleName, subject, callbackHandler)
: new LoginContext(_loginModuleName, subject, callbackHandler, _configuration));

loginContext.login();
Expand All @@ -265,6 +233,10 @@ else if (callback instanceof ServletRequestCallback)
{
LOG.ignore(e);
}
finally
{
INSTANCE.remove();
}
return null;
}

Expand Down Expand Up @@ -305,53 +277,47 @@ public void logout(UserIdentity user)
*/
protected String[] getGroups(Subject subject)
{
List<String> roleNameList = Arrays.asList(getRoleClassNames());

Collection<String> groups = new LinkedHashSet<>();
Set<Principal> principals = subject.getPrincipals();
for (Principal principal : principals)
{
Class<?> c = principal.getClass();
while (c != null)
boolean added = false;
//check whether the type of this Principle is a role
while (c != null && !added)
{
if (roleClassNameMatches(c.getName()))
if (roleClassNameMatches(c, roleNameList))
{
groups.add(principal.getName());
break;
}

boolean added = false;
for (Class<?> ci : c.getInterfaces())
{
if (roleClassNameMatches(ci.getName()))
{
groups.add(principal.getName());
added = true;
break;
}
}

if (!added)
{
c = c.getSuperclass();
added = true;
}
else
break;
c = c.getSuperclass();
janbartel marked this conversation as resolved.
Show resolved Hide resolved
}
}

return groups.toArray(new String[groups.size()]);
}

private boolean roleClassNameMatches(String classname)

/**
* Check if a given class, or any of the interfaces that it implements is one of the role classes.
* We do this comparison by classnames, without loading the role classes.
* @param clazz the class and its interfaces to check
* @param roleClassNames class names of the role classes
* @return true if the class or one of its interfaces is one of the configured role classes
*/
private static boolean roleClassNameMatches(Class<?> clazz, List<String> roleClassNames)
{
boolean result = false;
for (String roleClassName : getRoleClassNames())
{
if (roleClassName.equals(classname))
{
result = true;
break;
}
}
return result;
if (clazz == null || roleClassNames == null)
return false;
//collect the names of the class and any interfaces it implements
List<String> classnames = new ArrayList<>();
classnames.add(clazz.getName());
Arrays.stream(clazz.getInterfaces()).map(i -> i.getName()).forEach(i -> classnames.add(i));

return roleClassNames.stream().filter(classnames::contains).distinct().count() > 0;

janbartel marked this conversation as resolved.
Show resolved Hide resolved
}
}
@@ -0,0 +1,101 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//

package org.eclipse.jetty.jaas;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import org.eclipse.jetty.jaas.spi.PropertyFileLoginModule;
import org.eclipse.jetty.security.PropertyUserStore;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

/**
* PropertyUserStoreManager
*
* Maintains a map of PropertyUserStores, keyed off the location of the property file containing
* the authentication and authorization information.
*
* This class is used to enable the PropertyUserStores to be cached and shared. This is essential
* for the PropertyFileLoginModules, whose lifecycle is controlled by the JAAS api and instantiated
* afresh whenever a user needs to be authenticated. Without this class, every PropertyFileLoginModule
* instantiation would re-read and reload in all the user information just to authenticate a single user.
*/
public class PropertyUserStoreManager extends AbstractLifeCycle
{
private static final Logger LOG = Log.getLogger(PropertyFileLoginModule.class);

/**
* Map of user authentication and authorization information loaded in from a property file.
* The map is keyed off the location of the file.
*/
private Map<String, PropertyUserStore> _propertyUserStores;

public PropertyUserStore getPropertyUserStore(String file)
{
synchronized (this)
{
if (_propertyUserStores == null)
return null;

return _propertyUserStores.get(file);
}
}

public PropertyUserStore addPropertyUserStore(String file, PropertyUserStore store)
{
synchronized (this)
{
Objects.requireNonNull(_propertyUserStores);
PropertyUserStore existing = _propertyUserStores.get(file);
if (existing != null)
return existing;

_propertyUserStores.put(file, store);
return store;
}
}

@Override
protected void doStart() throws Exception
{
_propertyUserStores = new HashMap<String, PropertyUserStore>();
super.doStart();
}

@Override
protected void doStop() throws Exception
{
for (Map.Entry<String,PropertyUserStore> entry: _propertyUserStores.entrySet())
{
try
{
entry.getValue().stop();
}
catch (Exception e)
{
LOG.warn("Error stopping PropertyUserStore at {}", entry.getKey(), e);
}
}
_propertyUserStores = null;
super.doStop();
}
}
Expand Up @@ -26,7 +26,6 @@
import javax.security.auth.callback.UnsupportedCallbackException;

import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.security.Password;

/**
* DefaultCallbackHandler
Expand All @@ -47,38 +46,34 @@ public void setRequest(Request request)
public void handle(Callback[] callbacks)
throws IOException, UnsupportedCallbackException
{
for (int i = 0; i < callbacks.length; i++)
for (Callback callback : callbacks)
{
if (callbacks[i] instanceof NameCallback)
if (callback instanceof NameCallback)
{
((NameCallback)callbacks[i]).setName(getUserName());
((NameCallback)callback).setName(getUserName());
}
else if (callbacks[i] instanceof ObjectCallback)
else if (callback instanceof ObjectCallback)
{
((ObjectCallback)callbacks[i]).setObject(getCredential());
((ObjectCallback)callback).setObject(getCredential());
}
else if (callbacks[i] instanceof PasswordCallback)
else if (callback instanceof PasswordCallback)
{
if (getCredential() instanceof Password)
((PasswordCallback)callbacks[i]).setPassword(getCredential().toString().toCharArray());
else if (getCredential() instanceof String)
{
((PasswordCallback)callbacks[i]).setPassword(((String)getCredential()).toCharArray());
}
else
throw new UnsupportedCallbackException(callbacks[i], "User supplied credentials cannot be converted to char[] for PasswordCallback: try using an ObjectCallback instead");
((PasswordCallback)callback).setPassword(getCredential().toString().toCharArray());
}
else if (callbacks[i] instanceof RequestParameterCallback)
else if (callback instanceof RequestParameterCallback)
{
RequestParameterCallback callback = (RequestParameterCallback)callbacks[i];
callback.setParameterValues(Arrays.asList(_request.getParameterValues(callback.getParameterName())));
if (_request != null)
{
RequestParameterCallback rpc = (RequestParameterCallback)callback;
rpc.setParameterValues(Arrays.asList(_request.getParameterValues(rpc.getParameterName())));
}
}
else if (callbacks[i] instanceof ServletRequestCallback)
else if (callback instanceof ServletRequestCallback)
{
((ServletRequestCallback)callbacks[i]).setRequest(_request);
((ServletRequestCallback)callback).setRequest(_request);
}
else
throw new UnsupportedCallbackException(callbacks[i]);
throw new UnsupportedCallbackException(callback);
}
}
}
Expand Down