package org.dcache.gplazma.plugins;
import com.sun.jna.ptr.IntByReference;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.core.classloader.annotations.SuppressStaticInitializationFor;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;
import java.security.Principal;
import java.util.HashSet;
import java.util.Set;
import org.dcache.auth.GidPrincipal;
import org.dcache.auth.GroupNamePrincipal;
import org.dcache.auth.UidPrincipal;
import org.dcache.auth.UserNamePrincipal;
import org.dcache.auth.attributes.HomeDirectory;
import org.dcache.auth.attributes.RootDirectory;
import org.dcache.gplazma.AuthenticationException;
import org.dcache.gplazma.NoSuchPrincipalException;
import org.dcache.gplazma.plugins.Nsswitch.LibC;
import org.dcache.gplazma.plugins.Nsswitch.__group;
import org.dcache.gplazma.plugins.Nsswitch.__password;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.powermock.api.mockito.PowerMockito.whenNew;
import static com.google.common.collect.Sets.newHashSet;
import static com.google.common.base.Preconditions.checkState;
@RunWith(PowerMockRunner.class)
@SuppressStaticInitializationFor({"com.sun.jna.Structure"})
@PrepareForTest(Nsswitch.class)
/**
* Tests for the Nsswitch gPlazma plugin. This plugin uses JNA to invoke
* various libc methods to discover the host system's configuration for
* mapping usernames and groupnames to uids and gids, respectively.
*
* For testing purposes, this makes testing awkward for several reasons:
*
* 1. unit tests should not depend on JVM-platform features. JNA isn't
* supported by all JVM platforms.
*
* 2. unit tests should not depend on OS-specific functionality. OSes exist
* that don't provide the required libc functions.
*
* 2. unit tests should not depend on external configuration. On most Unix-
* like systems, there exists a user with {name=root, uid=0, gid=0},
* however this is insufficient for testing all functionality and
* additional mappings are much less certain.
*
* This class mocks the calls to the LibC class to return predefined responses,
* so allowing arbitrary testing, independent of underlying JVM and OS.
* Unfortunately, doing this requires employing a "large hammer" (in the
* form of PowerMock) to beat the dependent classes into submission.
*/
public class NsswitchTest
{
private LibC _libc;
private Nsswitch _plugin;
private Principal _identityMapResult;
private Set<Principal> _identityReverseMapResult;
private Set<Principal> _loginMapResult;
private Set<Object> _attributes;
@Before
public void setUp() throws Exception
{
_libc = mock(LibC.class);
_plugin = new Nsswitch(_libc);
_identityMapResult = null;
_identityReverseMapResult = null;
_loginMapResult = null;
_attributes = null;
IntByReference intByRef = Whitebox.newInstance(SimpleIntStorage.class);
whenNew(IntByReference.class).withNoArguments().thenReturn(intByRef);
}
@Test
public void shouldIdentityMapNameToUid() throws NoSuchPrincipalException
{
given(aUser().withName("kermit").withUid(100));
whenIdentityMap(userNamePrincipal("kermit"));
assertThat(_identityMapResult, is(equalTo(uidPrincipal(100))));
}
@Test(expected=NoSuchPrincipalException.class)
public void shouldFailWhenIdentityMapUnknownName()
throws NoSuchPrincipalException
{
given(noUser().withName("kermit"));
whenIdentityMap(userNamePrincipal("kermit"));
}
@Test
public void shouldIdentityReverseMapUidToName()
throws NoSuchPrincipalException
{
given(aUser().withName("kermit").withUid(100));
whenIdentityReverseMap(uidPrincipal(100));
assertThat(_identityReverseMapResult, hasItem(userNamePrincipal("kermit")));
}
@Test(expected=NoSuchPrincipalException.class)
public void shouldFailWhenIdentityReverseMapUnknownUid()
throws NoSuchPrincipalException
{
given(noUser().withUid(100));
whenIdentityReverseMap(uidPrincipal(100));
}
@Test
public void shouldIdentityMapNameToGid() throws NoSuchPrincipalException
{
given(aGroup().withName("it").withGid(200));
whenIdentityMap(groupNamePrincipal("it"));
// REVISIT: should a group name be mapped to a primary gid?
assertThat(_identityMapResult, is(nonPrimaryGidPrincipal(200)));
}
@Test(expected=NoSuchPrincipalException.class)
public void shouldFailWhenIdentityMapUnknownGroupName()
throws NoSuchPrincipalException
{
given(noGroup().withName("it"));
whenIdentityMap(groupNamePrincipal("it"));
}
@Test
public void shouldIdentityReverseMapPrimaryGidToName()
throws NoSuchPrincipalException
{
given(aGroup().withName("it").withGid(200));
whenIdentityReverseMap(primaryGidPrincipal(200));
assertThat(_identityReverseMapResult, hasItem(groupNamePrincipal("it")));
}
@Test
public void shouldIdentityReverseMapGidToName()
throws NoSuchPrincipalException
{
given(aGroup().withName("it").withGid(200));
whenIdentityReverseMap(nonPrimaryGidPrincipal(200));
assertThat(_identityReverseMapResult, hasItem(groupNamePrincipal("it")));
}
@Test
public void shouldLoginMapForUserWithSingleGroup()
throws AuthenticationException
{
given(aUser().withName("kermit").withUid(100).withGid(200));
// REVISIT consider using PrincipalSetMaker
whenLoginMap(newHashSet(userNamePrincipal("kermit")));
assertThat(_loginMapResult, hasItem(uidPrincipal(100)));
assertThat(_loginMapResult, hasItem(primaryGidPrincipal(200)));
}
@Test
public void shouldLoginMapForUserWithMultipleGroups()
throws AuthenticationException
{
given(aUser().withName("kermit").withUid(100).withGid(200).withExtraGids(210,220));
// REVISIT consider using PrincipalSetMaker
whenLoginMap(newHashSet(userNamePrincipal("kermit")));
assertThat(_loginMapResult, hasItem(uidPrincipal(100)));
assertThat(_loginMapResult, hasItem(primaryGidPrincipal(200)));
assertThat(_loginMapResult, hasItem(nonPrimaryGidPrincipal(210)));
assertThat(_loginMapResult, hasItem(nonPrimaryGidPrincipal(220)));
}
@Test
public void shouldLoginSession() throws AuthenticationException
{
// no "given"s since behaviour is independent of system state
whenLoginSession(null); // null is OK since principals are ignored
assertThat(_attributes, hasItem(homeDirectory("/")));
assertThat(_attributes, hasItem(rootDirectory("/")));
}
private void whenIdentityMap(Principal p) throws NoSuchPrincipalException
{
_identityMapResult = _plugin.map(p);
}
private void whenIdentityReverseMap(Principal p) throws NoSuchPrincipalException
{
_identityReverseMapResult = _plugin.reverseMap(p);
}
private void whenLoginMap(Set<Principal> principals) throws AuthenticationException
{
_loginMapResult = new HashSet<>(principals);
_plugin.map(_loginMapResult);
}
private void whenLoginSession(Set<Principal> principals) throws AuthenticationException
{
_attributes = new HashSet<>();
_plugin.session(principals, _attributes);
}
private void given(UserInfo user)
{
if (user.hasName()) {
when(_libc.getpwnam(user.getName())).thenReturn(user.buildPassword());
}
if (user.hasUid()) {
when(_libc.getpwuid(user._uid)).thenReturn(user.buildPassword());
}
if (user.hasName() && user.hasGid()) {
when(_libc.getgrouplist(eq(user.getName()), eq(user.getGid()),
any(int[].class), any(IntByReference.class))).
thenAnswer(new GetGroupListAnswer(user._extraGids));
}
}
private void given(GroupInfo group)
{
if (group.hasName()) {
when(_libc.getgrnam(group.getName())).thenReturn(group.buildGroup());
}
if (group.hasGid()) {
when(_libc.getgrgid(group.getGid())).thenReturn(group.buildGroup());
}
}
private Principal uidPrincipal(int uid)
{
return new UidPrincipal(uid);
}
private Principal primaryGidPrincipal(int uid)
{
return new GidPrincipal(uid, true);
}
private Principal nonPrimaryGidPrincipal(int uid)
{
return new GidPrincipal(uid, false);
}
private Principal userNamePrincipal(String name)
{
return new UserNamePrincipal(name);
}
private Principal groupNamePrincipal(String name)
{
return new GroupNamePrincipal(name);
}
private Object homeDirectory(String dir)
{
return new HomeDirectory(dir);
}
private Object rootDirectory(String dir)
{
return new RootDirectory(dir);
}
private UserInfo aUser()
{
return new UserInfo();
}
private UserInfo noUser()
{
return new UserInfo().isAbsent();
}
private GroupInfo aGroup()
{
return new GroupInfo();
}
private GroupInfo noGroup()
{
return new GroupInfo().isAbsent();
}
/**
* Fluent class for collecting information about a POSIX user, which is
* used to build a __password object. It is also used to hold additional
* group membership (the gids) of this user.
*/
private class UserInfo
{
String _name;
boolean _hasName;
int _uid;
boolean _hasUid;
int _gid;
boolean _hasGid;
int[] _extraGids = new int[0];
boolean _isAbsent;
UserInfo withName(String name)
{
_name = name;
_hasName = true;
return this;
}
UserInfo withUid(int uid)
{
_uid = uid;
_hasUid = true;
return this;
}
UserInfo withGid(int gid)
{
_gid = gid;
_hasGid = true;
return this;
}
UserInfo withExtraGids(int... gids)
{
_extraGids = gids;
return this;
}
UserInfo isAbsent()
{
_isAbsent = true;
return this;
}
boolean hasName()
{
return _hasName;
}
boolean hasUid()
{
return _hasUid;
}
boolean hasGid()
{
return _hasGid;
}
String getName()
{
checkState(_hasName, "no name");
return _name;
}
int getUid()
{
checkState(_hasUid, "no uid");
return _uid;
}
int getGid()
{
checkState(_hasGid, "no gid");
return _gid;
}
__password buildPassword()
{
if (_isAbsent) {
return null;
}
__password password = Whitebox.newInstance(__password.class);
password.name = _name;
password.uid = _uid;
password.gid = _gid;
return password;
}
}
/**
* Fluent class to hold information about a POSIX group and build a
* corresponding __group object.
*/
private class GroupInfo
{
private String _name;
private int _gid;
private boolean _hasName;
private boolean _hasGid;
private boolean _isAbsent;
GroupInfo withName(String name)
{
_name = name;
_hasName = true;
return this;
}
GroupInfo withGid(int gid)
{
_gid = gid;
_hasGid = true;
return this;
}
GroupInfo isAbsent()
{
_isAbsent = true;
return this;
}
boolean hasName()
{
return _hasName;
}
boolean hasGid()
{
return _hasGid;
}
int getGid()
{
checkState(_hasGid, "gid not set");
return _gid;
}
String getName()
{
checkState(_hasName, "name not set");
return _name;
}
__group buildGroup()
{
if (_isAbsent) {
return null;
}
__group group = Whitebox.newInstance(__group.class);
group.name = _name;
group.gid = _gid;
return group;
}
}
/**
* Class to hold login for the getgrouplist method of _libc. The Nsswitch
* class makes use of the getgrouplist(3) function, which uses
* negotiation to establishing how many groups a user has membership. In
* general, the Nsswitch class invokes getgrouplist twice; once to discover
* the number of gids and the second time to acquire the list. Therefore
* logic is needed to respond correctly.
*/
private class GetGroupListAnswer implements Answer
{
private final int[] _gids;
GetGroupListAnswer(int[] gids)
{
_gids = gids;
}
@Override
public Object answer(InvocationOnMock invocation)
{
Object[] args = invocation.getArguments();
IntByReference ngroups = (IntByReference) args[3];
int count = _gids.length;
if (ngroups.getValue() < count) {
ngroups.setValue(count);
return Integer.valueOf(-1);
}
int[] gids = (int[]) args[2];
System.arraycopy(_gids, 0, gids, 0, count);
return Integer.valueOf(count);
}
}
/**
* The IntByReference class provides pointer-like behaviour that allows
* a method to update the supplied argument. The JNA implementation
* achieves this using JNA code, which we wish to avoid. This method
* is a stand-in replacement that, while useless for JNA, provides the same
* Java-side functionality. Although this class works, it requires
* additional PowerMock magic to suppress the default constructor of the
* super class.
*/
private class SimpleIntStorage extends IntByReference
{
private int _value;
@Override
public int getValue()
{
return _value;
}
@Override
public void setValue(int value)
{
_value = value;
}
}
}