package org.dcache.gplazma.plugins;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Ordering;
import eu.emi.security.authn.x509.X509CertChainValidatorExt;
import eu.emi.security.authn.x509.X509Credential;
import eu.emi.security.authn.x509.impl.PEMCredential;
import eu.emi.security.authn.x509.proxy.ProxyUtils;
import org.apache.axis.AxisEngine;
import org.apache.axis.ConfigurationException;
import org.apache.axis.SimpleTargetedChain;
import org.apache.axis.client.Call;
import org.apache.axis.configuration.SimpleProvider;
import org.italiangrid.voms.VOMSAttribute;
import org.italiangrid.voms.VOMSValidators;
import org.italiangrid.voms.ac.VOMSACValidator;
import org.italiangrid.voms.store.VOMSTrustStore;
import org.italiangrid.voms.store.VOMSTrustStores;
import org.italiangrid.voms.util.CertificateValidatorBuilder;
import org.opensciencegrid.authz.xacml.client.MapCredentialsClient;
import org.opensciencegrid.authz.xacml.common.LocalId;
import org.opensciencegrid.authz.xacml.common.XACMLConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.SocketException;
import java.security.KeyStoreException;
import java.security.Principal;
import java.security.cert.CertPath;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Hashtable;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.dcache.auth.LoginGidPrincipal;
import org.dcache.auth.LoginNamePrincipal;
import org.dcache.auth.UserNamePrincipal;
import org.dcache.gplazma.AuthenticationException;
import org.dcache.gplazma.util.CertPaths;
import org.dcache.srm.client.HttpClientSender;
import org.dcache.srm.client.HttpClientTransport;
import org.dcache.ssl.CanlContextFactory;
import org.dcache.util.NetworkUtils;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Predicates.instanceOf;
import static com.google.common.collect.Iterables.any;
import static com.google.common.collect.Iterables.find;
import static eu.emi.security.authn.x509.impl.OpensslNameUtils.convertFromRfc2253;
import static java.util.Arrays.asList;
import static org.dcache.gplazma.util.CertPaths.getOriginalUserDnAsGlobusPrincipal;
import static org.dcache.gplazma.util.CertPaths.isX509CertPath;
import static org.dcache.gplazma.util.Preconditions.checkAuthentication;
import static org.dcache.util.ByteUnit.KiB;
/**
* Responsible for taking an X509Certificate chain from the public credentials
* and adding a {@link UserNamePrincipal} based on a mapping for the local
* storage resource returned from a GUMS/XACML service.<br>
* <br>
*
* The authentication method is an alternative to straight VOMS authentication;
* it requires that the X509 proxy contain the following VOMS extensions: <br>
* <br>
*
* <ul>
* <li>VO</li>
* <li>VOMS subject</li>
* <li>VOMS issuer</li>
* <li>attribute (FQAN)</li>
* </ul>
* <br>
*
* The gplazma.conf file definition line for this plugin can contain the
* following property definitions: <br>
*
* <table>
* <tr>
* <th>PROPERTY</th>
* <th>DEFAULT VALUE</th>
* <th>DESCRIPTION</th>
* </tr>
* <tr>
* <td>gplazma.vomsdir.dir</td>
* <td>/etc/grid-security/vomsdir</td>
* <td>location of VOMS authority subdirs & .lsc files</td>
* </tr>
* <tr>
* <td>gplazma.vomsdir.ca</td>
* <td>/etc/grid-security/certificates</td>
* <td>location of CA certs used in VOMS validation</td>
* </tr>
* <tr>
* <td>gplazma.xacml.service.url</td>
* <td>(required)</td>
* <td>location of the XACML service to contact for mapping</td>
* </tr>
* <tr>
* <td>gplazma.xacml.client.type</td>
* <td><code>org.dcache.gplazma.plugins.PrivilegeDelegate</code></td>
* <td>client implementation (the default is a simple wrapper around
* <code>org.opensciencegrid.authz.xacml.client.MapCredentialsClient</code>)</td>
* </tr>
* <tr>
* <td>gplazma.xacml.cachelife.secs</td>
* <td>30</td>
* <td>time-to-live in local (in-memory) cache (between accesses) for a mapping
* entry already fetched from the XACML service</td>
* </tr>
* <tr>
* <td>gplazma.xacml.cache.maxsize</td>
* <td>1024</td>
* <td>maximum entries held in the cache</td>
* </tr>
* </table>
*
* @author arossi
*/
public final class XACMLPlugin implements GPlazmaAuthenticationPlugin
{
/**
* Simple struct to hold the extensions extracted from the certificate
* chain.
*
* Attribute formats are described in [1] "An XACML Attribute and Obligation
* Profile for Authorization Interoperability in Grids". Relevant parts are
* quoted below.
*
* @author arossi
*/
private static class VomsExtensions {
/**
* From [1]: This attribute holds the Distinguished Name (DN) of the user. This DN is the subject extracted
* from the user’s certificate. This attribute is implicitly linked in this profile with subject-x509-issuer
* attribute. The datatype of this attribute is a string, to accommodate the OpenSSL one-line representation
* of slash-separated Relative Distinguished Names.
* <p>
* We acknowledge that the most commonly used representation for this attribute is the X.500 datatype;
* however, we decide not to use it because the slash-separated representation is the defacto standard
* in our environment. Tools and services are free to support the subject-x509-id as X.500 datatype
* besides the OpenSSL online representation. In that case the Datatype MUST be set to X509Name.
*/
private String _x509Subject;
/**
* From [1]: This attribute holds the Distinguished Name (DN) of the CA that signed the end entity user
* certificate. This DN is extracted from the user’s certificate and it is implicitly linked to the
* subject-x509 attribute-id. The datatype of this attribute is string, for the same reasons argued in the
* Subject-x509-id attribute.
*/
private String _x509SubjectIssuer;
/**
* From [1]: VOMS maintains the organizational structure of a VO in hierarchical groups. Users can belong
* to such groups and can have specific roles for each group. In the Attribute Certificate, the membership
* to a group with a role is encoded as a Fully Qualified Attribute Name (FQAN). This attribute holds one
* FQAN from the VOMS Attribute Certificate in the user credentials. Because users typically belong to
* several groups, this attribute can be set many times to encode all FQAN in the AC. For this profile,
* the order of the FQAN is not relevant, considering that the primary FQAN of the user is conveyed
* through the attribute VOMS-Primary-FQAN.
* <p>
* The PDP SHOULD perform a direct string match of the VOMS FQAN values when it evaluates an authorization
* request against its policy. VOMS FQANs have an optional suffix, e.g. /Role=NULL. A PDP COULD implement
* the VOMS matching rules to ignore these type of suffixes.
*/
private String _fqan;
private boolean _primary;
/**
* From [1]: This attribute holds the name of the first user Virtual Organization (VO) found in the set of
* attribute certificates. The user is requesting authorization in virtue of her membership to this VO,
* project or community. There are two methods for extracting the VO name from an Attribute Certificate
* (AC): (1) from the “VO” attribute of the VOMS AC; (2) from the left-most slash- separated portion of
* the Fully Qualified Attribute Names (FQAN) [FQAN] attributes. This attribute contains the name extracted
* from the VO attribute of the AC (method 1).
* <p>
* From our experience multiple simultaneous VO usage has not observed the use case. All the VO specific
* attributes in VOMS (more of these will follow in the document) are describing the top VO which is
* represented explicitly in the VOMS-PRIMARY-FQAN attribute. The VOMS FQANs from all the potentially
* conveyed VOs CAN be expressed in the VOMS-FQAN attribute.
*/
private String _vo;
/**
* From [1]: VOMS-signing-subject holds the DN of the VOMS service that signed the first Attribute
* Certificate in the user credentials. It is extracted from the “issuer” attribute of the VOMS AC
* and is implicitly linked in this profile to the VOMS-signing-issuer attribute. As evident by its
* name, this attribute (and the others with similar names in this profile) is designed to convey
* information about an authoritative membership service implemented via a VOMS service. Other
* membership service implementations can still use this profile provided that their concepts can
* be properly described by the semantics of these attributes.
*/
private String _vomsSigningSubject;
/**
* From [1]: Considering that VOMS ACs are signed by a VOMS certificate, VOMS-signing-issuer holds
* the DN of the CA that signed that VOMS certificate. This attribute does not provide information
* about the whole trust chain: it provides only the DN of the CA that issued the first VOMS attribute
* certificate. VOMS-signing-issuer is implicitly linked in this profile to the VOMS-signing-subject
* attribute. It can be extracted programmatically using the VOMS API and is not displayed in typical
* command line tools, like voms-proxy-info.
*/
private String _vomsSigningIssuer;
private VomsExtensions(String x509Subject,
String x509SubjectIssuer, String vo, String vomsSigningSubject,
String vomsSigningIssuer, String fqan, boolean primary) {
_x509Subject = x509Subject;
_x509SubjectIssuer = x509SubjectIssuer;
_vo = vo;
_vomsSigningSubject = vomsSigningSubject;
_vomsSigningIssuer = vomsSigningIssuer;
_fqan = fqan;
_primary = primary;
}
@Override
public boolean equals(Object object) {
if (!(object instanceof VomsExtensions)) {
return false;
}
return toString().equals(object.toString());
}
@Override
public int hashCode() {
return toString().hashCode();
}
@Override
public String toString() {
return VomsExtensions.class.getSimpleName() + "[X509Subject='"
+ _x509Subject + "', X509SubjectIssuer='"
+ _x509SubjectIssuer + "', fqan='" + _fqan
+ "', primary=" + _primary + ", VO='" + _vo
+ "', VOMSSubject='" + _vomsSigningSubject
+ "', VOMSSubjectIssuer='" + _vomsSigningIssuer
+ "']";
}
}
/**
* Does the work of contacting the XACML server to get a mapping not in the
* cache.
*
* @author arossi
*/
private class XACMLFetcher extends CacheLoader<VomsExtensions, LocalId> {
/*
* (non-Javadoc) Contacts the XACML/GUMS service. Throws Authentication
* Exception if an exception occurs or no mapping is found.
*
* @see com.google.common.cache.CacheLoader#load(java.lang.Object)
*/
@Override
public LocalId load(VomsExtensions key) throws AuthenticationException {
IMapCredentialsClient xacmlClient = newClient();
xacmlClient.configure(_properties);
xacmlClient.setX509Subject(key._x509Subject);
xacmlClient.setX509SubjectIssuer(key._x509SubjectIssuer);
xacmlClient.setFqan(key._fqan);
xacmlClient.setVO(key._vo);
xacmlClient.setVOMSSigningSubject(key._vomsSigningSubject);
xacmlClient.setVOMSSigningIssuer(key._vomsSigningIssuer);
xacmlClient.setResourceType(XACMLConstants.RESOURCE_SE);
xacmlClient.setResourceDNSHostName(_resourceDNSHostName);
xacmlClient.setResourceX509ID(_targetServiceName);
xacmlClient.setResourceX509Issuer(_targetServiceIssuer);
xacmlClient.setRequestedaction(XACMLConstants.ACTION_ACCESS);
xacmlClient.setAxisConfiguration(axisConfiguration);
LocalId localId = xacmlClient.mapCredentials(_mappingServiceURL);
Preconditions.checkArgument(localId != null, DENIED_MESSAGE + key);
logger.debug("mapping service {} returned localId {} for {} ",
_mappingServiceURL, localId, key);
return localId;
}
}
static final String VOMSDIR = "gplazma.xacml.vomsdir";
static final String ILLEGAL_CACHE_SIZE = "cache size must be non-zero positive integer; was: ";
static final String ILLEGAL_CACHE_LIFE = "cache life must be positive integer; was: ";
static final String DENIED_MESSAGE = "Permission Denied: "
+ "No XACML mapping retrieved for extensions ";
static final String HOST_CREDENTIAL_ERROR = "Could not load host globus credentials ";
static final String SERVICE_URL_PROPERTY = "gplazma.xacml.service.url";
static final String CLIENT_TYPE_PROPERTY = "gplazma.xacml.client.type";
static final String SERVICE_KEY = "gplazma.xacml.hostkey";
static final String SERVICE_CERT = "gplazma.xacml.hostcert";
static final String CADIR = "gplazma.xacml.ca";
static final String CACHE_LIFETIME = "gplazma.xacml.cachelife.secs";
static final String CACHE_SIZE = "gplazma.xacml.cache.maxsize";
private static final Logger logger = LoggerFactory.getLogger(XACMLPlugin.class);
/*
* caching enabled by default
*/
private static final String DEFAULT_CACHE_LIFETIME = "30";
/*
* Optimization for rapid sequential storage operation requests. Cache is
* first searched before going to the (remote) XACML service. Each entry has
* a short time-to-live by default (30 seconds).
*/
private LoadingCache<VomsExtensions, LocalId> _localIdCache;
/*
* the XACML service
*/
private final String _mappingServiceURL;
/*
* passed to XACML client configure()
*/
private final Properties _properties;
/*
* for XACML client configuration
*/
private Class<? extends PrivilegeDelegate> _clientType;
private String _targetServiceName;
private String _targetServiceIssuer;
private String _resourceDNSHostName;
private SimpleProvider axisConfiguration;
/*
* VOMS setup
*/
private VOMSACValidator validator;
static {
Call.setTransportForProtocol("http", HttpClientTransport.class);
Call.setTransportForProtocol("https", HttpClientTransport.class);
}
/**
* Configures VOMS extension validation, XACML service location, local id
* caching and storage resource information.
*/
public XACMLPlugin(Properties properties) {
_properties = properties;
_mappingServiceURL = properties.getProperty(SERVICE_URL_PROPERTY);
}
@Override
public void start() throws ClassNotFoundException, IOException, CertificateException, KeyStoreException
{
String caDir = _properties.getProperty(CADIR);
String vomsDir = _properties.getProperty(VOMSDIR);
checkArgument(caDir != null, "Undefined property: " + CADIR);
checkArgument(vomsDir != null, "Undefined property: " + VOMSDIR);
VOMSTrustStore vomsTrustStore = VOMSTrustStores.newTrustStore(asList(vomsDir));
X509CertChainValidatorExt certChainValidator = new CertificateValidatorBuilder().trustAnchorsDir(caDir).build();
validator = VOMSValidators.newValidator(vomsTrustStore, certChainValidator);
X509Credential credential = new PEMCredential(_properties.getProperty(SERVICE_KEY),
_properties.getProperty(SERVICE_CERT),
null);
_targetServiceName = convertFromRfc2253(credential.getCertificate().getSubjectX500Principal().getName(), true);
_targetServiceIssuer = convertFromRfc2253(credential.getCertificate().getIssuerX500Principal().getName(), true);
/*
* XACML setup
*/
checkArgument(_mappingServiceURL != null, "Undefined property: " + SERVICE_URL_PROPERTY);
setClientType(_properties.getProperty(CLIENT_TYPE_PROPERTY));
configureResourceDNSHostName();
/*
* AXIS configuration
*/
HttpClientSender sender = new HttpClientSender();
sender.setSslContextFactory(CanlContextFactory.custom().withCertificateAuthorityPath(caDir).build());
sender.init();
Hashtable<String,Object> options = new Hashtable<>();
options.put(HttpClientTransport.TRANSPORT_HTTP_CREDENTIALS, credential);
options.put(Call.SESSION_MAINTAIN_PROPERTY, true);
axisConfiguration = new SimpleProvider() {
@Override
public void configureEngine(AxisEngine engine) throws ConfigurationException
{
engine.refreshGlobalOptions();
}
};
axisConfiguration.deployTransport(HttpClientTransport.DEFAULT_TRANSPORT_NAME, new SimpleTargetedChain(sender));
axisConfiguration.setGlobalOptions(options);
/*
* LocalId Cache setup
*/
configureCache();
logger.debug("XACML plugin now loaded for URL {}", _mappingServiceURL);
}
@Override
public void stop()
{
validator.shutdown();
}
/*
* (non-Javadoc) Combines authentication and XACML mapping into one step by
* extracting (and optionally validating) the VOMS extensions necessary for
* the XACML client configuration, then retrieving the (first valid) mapping
* from the XACML service and adding it as a UserNamePrincipal to the
* identified principals. Note that if there already exists a
* UserNamePrincipal, an AuthenticationException is thrown.
*
* Calls {@link #extractExensionsFromChain(X509Certificate[], Set,
* VOMSValidator)} and {@link #getMappingFor(Set)}.
*/
@Override
public void authenticate(Set<Object> publicCredentials,
Set<Object> privateCredentials,
Set<Principal> identifiedPrincipals)
throws AuthenticationException {
checkAuthentication(
!any(identifiedPrincipals, instanceOf(UserNamePrincipal.class)),
"username already defined");
Set<VomsExtensions> extensions = new LinkedHashSet<>();
/*
* extract all sets of extensions from certificate chains
*/
for (Object credential : publicCredentials) {
if (isX509CertPath(credential)) {
CertPath certPath = (CertPath) credential;
identifiedPrincipals.add(getOriginalUserDnAsGlobusPrincipal(certPath));
extractExtensionsFromChain(certPath, extensions, validator);
}
}
logger.debug("VOMS extensions found: {}", extensions);
checkAuthentication(!extensions.isEmpty(), "no subjects found to map");
Principal login
= find(identifiedPrincipals, instanceOf(LoginNamePrincipal.class), null);
/*
* retrieve the first valid mapping and add it to the identified
* principals
*/
final LocalId localId = getMappingFor(login, extensions);
checkAuthentication(localId != null, "no mapping for: " + extensions);
checkAuthentication(localId.getUserName() != null, "no mapping for: " + extensions);
identifiedPrincipals.add(new UserNamePrincipal(localId.getUserName()));
if (localId.getGID() != null) {
identifiedPrincipals.add(new LoginGidPrincipal(localId.getGID()));
}
}
/**
* Sets up the local id cache.
*
* @throws IllegalArgumentException
* if the CACHE_LIFETIME is set to <0
*/
private void configureCache() throws IllegalArgumentException {
int expiry
= Integer.parseInt(_properties.getProperty(CACHE_LIFETIME,
DEFAULT_CACHE_LIFETIME));
if (expiry < 0) {
throw new IllegalArgumentException(ILLEGAL_CACHE_LIFE + expiry);
}
int size = _properties.containsKey(CACHE_SIZE)
? Integer.parseInt(_properties.getProperty(CACHE_SIZE))
: KiB.toBytes(1);
if (size < 1) {
throw new IllegalArgumentException(ILLEGAL_CACHE_SIZE + size);
}
/*
* constructed using strong references because the identity of the
* extension set is based on String equals, not on instance ==.
*/
_localIdCache = CacheBuilder.newBuilder()
.expireAfterAccess(expiry, TimeUnit.SECONDS)
.maximumSize(size)
.softValues()
.build(new XACMLFetcher());
}
/**
* Extracts canonical DNS name of storage resource host from network
* interfaces.
*
* @throws SocketException
*/
private void configureResourceDNSHostName() throws SocketException {
Iterable<InetAddress> addressList = NetworkUtils.getLocalAddresses();
try {
_resourceDNSHostName = Ordering.natural().onResultOf(NetworkUtils.InetAddressScope.OF).max(addressList).getCanonicalHostName();
} catch (NoSuchElementException ignored) {
}
}
/**
* Extracts VOMS extensions from the public credentials and adds them to the
* running list.
*/
@SuppressWarnings("deprecation")
private void extractExtensionsFromChain(CertPath certPath,
Set<VomsExtensions> extensionsSet,
VOMSACValidator validator)
throws AuthenticationException
{
X509Certificate[] chain = CertPaths.getX509Certificates(certPath);
X509Certificate eec = ProxyUtils.getEndUserCertificate(chain);
if (eec == null) {
throw new AuthenticationException("The checked certificate chain contains only proxy certificates.");
}
String x509Subject = convertFromRfc2253(eec.getSubjectX500Principal().getName(), true);
String x509SubjectIssuer = convertFromRfc2253(eec.getIssuerX500Principal().getName(), true);
List<VOMSAttribute> vomsAttributes = validator.validate(chain);
if (vomsAttributes.isEmpty()) {
VomsExtensions vomsExtensions =
new VomsExtensions(x509Subject, x509SubjectIssuer, null, null, null, null, true);
logger.trace(" {} authenticate, adding voms extensions = {}", this, vomsExtensions);
extensionsSet.add(vomsExtensions);
} else {
boolean primary = true;
for (VOMSAttribute vomsAttr : vomsAttributes) {
String vomsSigningSubject =
convertFromRfc2253(vomsAttr.getIssuer().getName(), true);
String vomsSigningIssuer =
convertFromRfc2253(vomsAttr.getAACertificates()[0].getIssuerX500Principal().getName(), true);
List<String> fqans = vomsAttr.getFQANs();
if (fqans.isEmpty()) {
fqans = Collections.singletonList(null);
}
for (String fqan : fqans) {
VomsExtensions vomsExtensions =
new VomsExtensions(x509Subject, x509SubjectIssuer, vomsAttr.getVO(),
vomsSigningSubject, vomsSigningIssuer, fqan, primary);
primary = false;
logger.trace(" {} authenticate, adding voms extensions = {}", this, vomsExtensions);
extensionsSet.add(vomsExtensions);
}
}
}
}
/**
* Convenience wrapper; loops through the set of extension groups and calls
* out to {@link LoadingCache#get(Object)}.
*
* @param login
* may be <code>null</code>
* @param extensionSet
* all groups of extracted VOMS extensions
* @return local id or <code>null</code> if no mapping is found
*/
private LocalId getMappingFor(Principal login,
Set<VomsExtensions> extensionSet) {
for (VomsExtensions extensions : extensionSet) {
try {
LocalId localId = _localIdCache.get(extensions);
String name = localId.getUserName();
if ( name == null ) {
continue;
}
if (login == null || login.getName().equals(name)) {
logger.debug("getMappingFor {} = {}", extensions, name);
return localId;
}
} catch (ExecutionException t) {
/*
* Exception has already been logged inside the fetcher ...
*/
logger.debug("could not find mapping for {}; continuing ...",
extensions);
}
}
logger.debug("no XACML mappings found for {}, {}", login, extensionSet);
return null;
}
/**
* Provides for possible alternate implementations of the XACML client by
* delegating to an implementation of {@link IMapCredentialsClient} which
* wraps the germane methods of the privilege class (
* {@link MapCredentialsClient}; privilege itself provides no interface).
*
* @return new instance of the class set from the
* <code>gplazma.xacml.client.type</code> property.
*/
private IMapCredentialsClient newClient() throws AuthenticationException {
try {
return _clientType.newInstance();
} catch (InstantiationException | IllegalAccessException t) {
throw new AuthenticationException(t.getMessage(), t);
}
}
/**
* If undefined, sets the default class.
*
* @param property
* as defined by <code>gplazma.xacml.client.type</code>; if
* <code>null</code>, the value which obtains is
* {@link PrivilegeDelegate}.
*/
private void setClientType(String property) throws ClassNotFoundException {
if (property == null || property.length() == 0) {
_clientType = PrivilegeDelegate.class;
} else {
_clientType = Class.forName(property, true,
Thread.currentThread().getContextClassLoader())
.asSubclass(PrivilegeDelegate.class);
}
}
}