package org.dcache.gplazma.plugins;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.primitives.Longs;
import java.io.IOException;
import java.security.Principal;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.dcache.auth.GidPrincipal;
import org.dcache.auth.GroupNamePrincipal;
import org.dcache.auth.LoginGidPrincipal;
import org.dcache.auth.LoginNamePrincipal;
import org.dcache.auth.LoginUidPrincipal;
import org.dcache.auth.UidPrincipal;
import org.dcache.auth.UserNamePrincipal;
import org.dcache.auth.attributes.HomeDirectory;
import org.dcache.auth.attributes.Restrictions;
import org.dcache.auth.attributes.RootDirectory;
import org.dcache.gplazma.AuthenticationException;
import org.dcache.gplazma.plugins.AuthzMapLineParser.UserAuthzInformation;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Predicates.instanceOf;
import static com.google.common.collect.Iterables.*;
import static org.dcache.gplazma.util.Preconditions.checkAuthentication;
/**
* Plugin uses AuthzDB for mapping group names and user names to UID,
* GIDs.
*
* User names are typically generated by mapping DNs or login names in
* another plugin.
*
* Group names are typically generated by mapping FQANs in another
* plugin and there will be a primary group name.
*
* LoginNamePrincipal, LoginUidPrincipal, and LoginGidPrincipal are
* used to pick one among multiple possible UIDs and to pick a primary
* group.
*/
public class AuthzDbPlugin
implements GPlazmaMappingPlugin, GPlazmaSessionPlugin
{
private static final long REFRESH_PERIOD =
TimeUnit.SECONDS.toMillis(10);
private static final String AUTHZDB =
"gplazma.authzdb.file";
private static final String UID =
"gplazma.authzdb.uid";
private static final String GID =
"gplazma.authzdb.gid";
enum PrincipalType { UID, GID, LOGIN, USER, GROUP }
private final ImmutableList<PrincipalType> _uidOrder;
private final ImmutableList<PrincipalType> _gidOrder;
private final SourceBackedPredicateMap<String,UserAuthzInformation> _map;
public AuthzDbPlugin(Properties properties) throws IOException
{
String path = properties.getProperty(AUTHZDB);
String uid = properties.getProperty(UID);
String gid = properties.getProperty(GID);
checkArgument(path != null, "Undefined property: " + AUTHZDB);
checkArgument(uid != null, "Undefined property: " + UID);
checkArgument(gid != null, "Undefined property: " + GID);
_map = new SourceBackedPredicateMap<>(new FileLineSource(path, REFRESH_PERIOD), new AuthzMapLineParser());
_uidOrder = parseOrder(uid);
_gidOrder = parseOrder(gid);
}
/**
* package visible constructor for testing purposes
* @param map map of usernames to user information (e.q. uid/gid)
*/
AuthzDbPlugin(SourceBackedPredicateMap<String,UserAuthzInformation> map,
ImmutableList<PrincipalType> uidOrder,
ImmutableList<PrincipalType> gidOrder)
{
_map = map;
_uidOrder = uidOrder;
_gidOrder = gidOrder;
}
static ImmutableList<PrincipalType> parseOrder(String s)
{
ImmutableList.Builder<PrincipalType> order = ImmutableList.builder();
for (String e: Splitter.on(',').omitEmptyStrings().split(s)) {
order.add(PrincipalType.valueOf(e.toUpperCase()));
}
return order.build();
}
private UserAuthzInformation getEntity(String name)
throws AuthenticationException
{
Collection<UserAuthzInformation> mappings =
_map.getValuesForPredicatesMatching(name);
checkAuthentication(!mappings.isEmpty(),
"no mapping exists for " + name);
return get(mappings, 0);
}
private UserAuthzInformation
getEntity(List<PrincipalType> order,
Long loginUid, Long loginGid, String loginName,
String userName, String primaryGroup)
throws AuthenticationException
{
for (PrincipalType type: order) {
switch (type) {
case UID:
if (loginUid != null) {
return new UserAuthzInformation(null, null, loginUid, null, null, null, null);
}
break;
case GID:
if (loginGid != null) {
return new UserAuthzInformation(null, null, 0, new long[] {loginGid}, null, null, null);
}
break;
case LOGIN:
if (loginName != null) {
return getEntity(loginName);
}
break;
case USER:
if (userName != null) {
return getEntity(userName);
}
break;
case GROUP:
if (primaryGroup != null) {
return getEntity(primaryGroup);
}
break;
}
}
throw new AuthenticationException("no mappable principal");
}
@Override
public void map(Set<Principal> principals)
throws AuthenticationException
{
/* Classify input principals.
*/
List<String> names = Lists.newArrayList();
String loginName = null;
Long loginUid = null;
Long loginGid = null;
String userName = null;
String primaryGroup = null;
for (Principal principal: principals) {
if (principal instanceof LoginNamePrincipal) {
checkAuthentication(loginName == null, "multiple login names");
loginName = principal.getName();
} else if (principal instanceof LoginUidPrincipal) {
checkAuthentication(loginUid == null, "multiple login UIDs");
loginUid = ((LoginUidPrincipal) principal).getUid();
} else if (principal instanceof LoginGidPrincipal) {
checkAuthentication(loginGid == null, "multiple login GIDs");
loginGid = ((LoginGidPrincipal) principal).getGid();
} else if (principal instanceof UserNamePrincipal) {
checkAuthentication(userName == null, "multiple usernames");
userName = principal.getName();
names.add(userName);
} else if (principal instanceof GroupNamePrincipal) {
if (((GroupNamePrincipal) principal).isPrimaryGroup()) {
checkAuthentication(primaryGroup == null,
"multiple primary group names");
primaryGroup = principal.getName();
}
names.add(principal.getName());
}
}
/* Determine the UIDs and GIDs available to the user
*/
List<Long> uids = Lists.newArrayList();
List<Long> gids = Lists.newArrayList();
for (String name: names) {
Collection<UserAuthzInformation> mappings =
_map.getValuesForPredicatesMatching(name);
for (UserAuthzInformation mapping: mappings) {
uids.add(mapping.getUid());
gids.addAll(Longs.asList(mapping.getGids()));
}
}
/* Verify that the login name, login UID and login GID are
* among the valid values.
*/
checkAuthentication(loginName == null || names.contains(loginName),
"not authorized to use login name: " + loginName);
checkAuthentication(loginUid == null || uids.contains(loginUid),
"not authorized to use UID: " + loginUid);
checkAuthentication(loginGid == null || gids.contains(loginGid),
"not authorized to use GID: " + loginGid);
/* Pick a UID and user name.
*/
UserAuthzInformation user =
getEntity(_uidOrder, loginUid, null, loginName, userName, primaryGroup);
principals.add(new UidPrincipal(user.getUid()));
if (user.getUsername() != null) {
// If UID is not based on user name but on some other principle, then it
// may be that the UserNamePrincipal is inconsistent with the UID. Since
// space manager uses user name for authorization, we replace the principal
// with the on matching the selected UID.
removeIf(principals, instanceOf(UserNamePrincipal.class));
principals.add(new UserNamePrincipal(user.getUsername()));
}
/* Pick a primary GID.
*/
UserAuthzInformation group =
getEntity(_gidOrder, null, loginGid, loginName, userName, primaryGroup);
long primaryGid = group.getGids()[0];
principals.add(new GidPrincipal(primaryGid, true));
/* Add remaining GIDs.
*/
for (long gid: gids) {
if (gid != primaryGid) {
principals.add(new GidPrincipal(gid, false));
}
}
}
@Override
public void session(Set<Principal> authorizedPrincipals,
Set<Object> attrib)
throws AuthenticationException
{
Principal principal =
find(authorizedPrincipals, instanceOf(UserNamePrincipal.class), null);
checkAuthentication(principal != null, "no username principal");
Collection<UserAuthzInformation> mappings
= _map.getValuesForPredicatesMatching(principal.getName());
checkAuthentication(!mappings.isEmpty(), "no mapping found for " + principal);
for (UserAuthzInformation mapping : mappings) {
attrib.add(new HomeDirectory(mapping.getHome()));
attrib.add(new RootDirectory(mapping.getRoot()));
if (mapping.isReadOnly()) {
attrib.add(Restrictions.readOnly());
}
}
}
}