/*
* Copyright (c) Members of the EGEE Collaboration. 2006-2010.
* See http://www.eu-egee.org/partners/ for details on the copyright holders.
*
* Licensed 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.glite.authz.pep.obligation.dfpmap;
import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.security.auth.x500.X500Principal;
import org.glite.authz.common.util.Strings;
import org.glite.authz.pep.obligation.ObligationProcessingException;
import org.glite.voms.PKIUtils;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.util.URIUtil;
import org.jruby.ext.posix.FileStat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A {@link PoolAccountManager} implementation that uses the filesystem as a
* persistence mechanism.
*
* The mapping directory must be prepopulated with files whose names represent
* every pool account to be managed.
*/
public class GridMapDirPoolAccountManager implements PoolAccountManager {
/** Class logger. */
private Logger log= LoggerFactory.getLogger(GridMapDirPoolAccountManager.class);
/** Directory containing the grid mappings. */
private final File gridMapDirectory_;
/**
* Determine the lease filename should contains the secondary group names or
* not.
* <p>
* Bug fix: https://savannah.cern.ch/bugs/?83317
*
* @see GridMapDirPoolAccountManager#buildSubjectIdentifier(X500Principal,
* String, List)
*/
private boolean useSecondaryGroupNamesForMapping_= true;
/**
* Regexp pattern used to identify pool account names.
* <p>
* Contains a single group match whose value is the pool account name
* prefix.
* <ul>
* <li>Bug fix: https://savannah.cern.ch/bugs/?66574
* <li>Bug fix: https://savannah.cern.ch/bugs/?80526
* </ul>
*/
private final Pattern poolAccountNamePattern_= Pattern.compile("^([a-zA-Z][a-zA-Z0-9._-]*?)[0-9]++$");
/**
* Constructor.
*
* @param gridMapDir
* existing, readable, and writable directory where grid mappings
* will be recorded
* @param useSecondaryGroupNamesForMapping
* if the lease filename in the gridmapDir should contains
* secondary group names or not
*/
public GridMapDirPoolAccountManager(File gridMapDir,
boolean useSecondaryGroupNamesForMapping) {
if (!gridMapDir.exists()) {
throw new IllegalArgumentException("Grid map directory "
+ gridMapDir.getAbsolutePath() + " does not exist");
}
if (!gridMapDir.canRead()) {
throw new IllegalArgumentException("Grid map directory "
+ gridMapDir.getAbsolutePath()
+ " is not readable by this process");
}
if (!gridMapDir.canWrite()) {
throw new IllegalArgumentException("Grid map directory "
+ gridMapDir.getAbsolutePath()
+ " is not writable by this process");
}
gridMapDirectory_= gridMapDir;
useSecondaryGroupNamesForMapping_= useSecondaryGroupNamesForMapping;
}
/** {@inheritDoc} */
public List<String> getPoolAccountNamePrefixes() {
ArrayList<String> poolAccountNames= new ArrayList<String>();
Matcher nameMatcher;
File[] files= gridMapDirectory_.listFiles();
for (File file : files) {
if (file.isFile()) {
nameMatcher= poolAccountNamePattern_.matcher(file.getName());
if (nameMatcher.matches()
&& !poolAccountNames.contains(nameMatcher.group(1))) {
poolAccountNames.add(nameMatcher.group(1));
}
}
}
return poolAccountNames;
}
/** {@inheritDoc} */
public List<String> getPoolAccountNames() {
return Arrays.asList(getAccountFileNames(null));
}
/** {@inheritDoc} */
public List<String> getPoolAccountNames(String prefix) {
return Arrays.asList(getAccountFileNames(Strings.safeTrimOrNullString(prefix)));
}
/** {@inheritDoc} */
public boolean isPoolAccountPrefix(String accountIndicator) {
return accountIndicator.startsWith(".");
}
/** {@inheritDoc} */
public String getPoolAccountPrefix(String accountIndicator) {
if (isPoolAccountPrefix(accountIndicator)) {
return accountIndicator.substring(1);
}
return null;
}
/**
* {@inheritDoc}
* <ul>
* <li>BUG FIX: https://savannah.cern.ch/bugs/index.php?83281
* <li>BUG FIX: https://savannah.cern.ch/bugs/index.php?84846
* </ul>
* */
public String mapToAccount(String accountNamePrefix,
X500Principal subjectDN, String primaryGroup,
List<String> secondaryGroups) throws ObligationProcessingException {
String subjectIdentifier= buildSubjectIdentifier(subjectDN,
primaryGroup,
secondaryGroups);
File subjectIdentifierFile= new File(buildSubjectIdentifierFilePath(subjectIdentifier));
log.debug("Checking if there is an existing account mapping for subject {} with primary group {} and secondary groups {}",
new Object[] { subjectDN.getName(), primaryGroup,
secondaryGroups });
String accountName= getAccountNameByKey(accountNamePrefix,
subjectIdentifier);
if (accountName != null) {
// BUG FIX: https://savannah.cern.ch/bugs/index.php?83281
// touch the subjectIdentifierFile every time a mapping is re-done.
PosixUtil.touchFile(subjectIdentifierFile);
log.debug("An existing account mapping has mapped subject {} with primary group {} and secondary groups {} to pool account {}",
new Object[] { subjectDN.getName(), primaryGroup,
secondaryGroups, accountName });
return accountName;
}
accountName= createMapping(accountNamePrefix, subjectIdentifier);
if (accountName != null) {
// BUG FIX: https://savannah.cern.ch/bugs/index.php?84846
// touch the subjectIdentifierFile the first time a mapping is done.
PosixUtil.touchFile(subjectIdentifierFile);
log.debug("A new account mapping has mapped subject {} with primary group {} and secondary groups {} to pool account {}",
new Object[] { subjectDN.getName(), primaryGroup,
secondaryGroups, accountName });
}
else {
log.debug("No pool account was available to which subject {} with primary group {} and secondary groups {} could be mapped",
new Object[] { subjectDN.getName(), primaryGroup,
secondaryGroups });
}
return accountName;
}
/**
* Gets the user account to which a given subject had previously been
* mapped.
*
* @param accountNamePrefix
* prefix of the account to which the subject should be mapped
* @param subjectIdentifier
* key identifying the subject
*
* @return account to which the subject was mapped or null if not map
* currently exists
*
* @throws ObligationProcessingException
* thrown if the link count on the pool account name file or the
* account key file is more than 2
*/
private String getAccountNameByKey(String accountNamePrefix,
String subjectIdentifier) throws ObligationProcessingException {
File subjectIdentifierFile= new File(buildSubjectIdentifierFilePath(subjectIdentifier));
// the file doesn't exit yet!!!
if (!subjectIdentifierFile.exists()) {
return null;
}
FileStat subjectIdentifierFileStat= PosixUtil.getFileStat(subjectIdentifierFile.getAbsolutePath());
if (subjectIdentifierFileStat.nlink() != 2) {
log.error("The subject identifier file "
+ subjectIdentifierFile.getAbsolutePath()
+ " has a link count greater than 2. This mapping is corrupted and can not be used.");
throw new ObligationProcessingException("Unable to map subject to a POSIX account");
}
FileStat accountFileStat;
for (File accountFile : getAccountFiles(accountNamePrefix)) {
accountFileStat= PosixUtil.getFileStat(accountFile.getAbsolutePath());
if (accountFileStat.ino() == subjectIdentifierFileStat.ino()) {
if (accountFileStat.nlink() != 2) {
log.error("The pool account file "
+ accountFile.getAbsolutePath()
+ " has a link count greater than 2. This mapping is corrupted and can not be used.");
}
return accountFile.getName();
}
}
return null;
}
/**
* Creates a mapping between an account and a subject identified by the
* account key.
*
* @param accountNamePrefix
* prefix of the pool account names
* @param subjectIdentifier
* key identifying the subject mapped to the account
*
* @return the account to which the subject was mapped or null if not
* account was available
*/
protected String createMapping(String accountNamePrefix,
String subjectIdentifier) {
FileStat accountFileStat;
for (File accountFile : getAccountFiles(accountNamePrefix)) {
log.debug("Checking if grid map account {} may be linked to subject identifier {}",
accountFile.getName(),
subjectIdentifier);
String subjectIdentifierFilePath= buildSubjectIdentifierFilePath(subjectIdentifier);
accountFileStat= PosixUtil.getFileStat(accountFile.getAbsolutePath());
if (accountFileStat.nlink() == 1) {
PosixUtil.createHardlink(accountFile.getAbsolutePath(),
subjectIdentifierFilePath);
accountFileStat= PosixUtil.getFileStat(accountFile.getAbsolutePath());
if (accountFileStat.nlink() == 2) {
log.debug("Linked subject identifier {} to pool account file {}",
subjectIdentifier,
accountFile.getName());
return accountFile.getName();
}
new File(subjectIdentifierFilePath).delete();
}
log.debug("Could not map to account {}", accountFile.getName());
}
log.warn("{} pool account is full. Impossible to map {}", accountNamePrefix,subjectIdentifier);
return null;
}
/**
* Creates an identifier (lease filename) for the subject that is based on
* the subject's DN and primary and secondary groups. The secondary groups
* are only included in the identifier if the
* {@link #useSecondaryGroupNamesForMapping_} is <code>true</code>.
* <p>
* Implements the legacy gLExec LCAS/LCMAP lease filename encoding.
* <ul>
* <li>BUG FIX: https://savannah.cern.ch/bugs/index.php?83419
* <li>Bug fix: https://savannah.cern.ch/bugs/?83317
* </ul>
*
* @param subjectDN
* DN of the subject
* @param primaryGroupName
* primary group to which the subject was assigned, may be null
* @param secondaryGroupNames
* ordered list of secondary groups to which the subject
* assigned, may be null
*
* @return the identifier for the subject
*/
protected String buildSubjectIdentifier(X500Principal subjectDN,
String primaryGroupName, List<String> secondaryGroupNames) {
StringBuilder identifier= new StringBuilder();
try {
String openSSLPrincipal= PKIUtils.getOpenSSLFormatPrincipal(subjectDN,
true);
// BUG FIX: https://savannah.cern.ch/bugs/index.php?83419
// encode using the legacy gLExec LCAS/LCMAP algorithm
String encodedId= encodeSubjectIdentifier(openSSLPrincipal);
identifier.append(encodedId);
} catch (URIException e) {
throw new RuntimeException("Charset required to be supported by JVM but is not available",
e);
}
if (primaryGroupName != null) {
identifier.append(":").append(primaryGroupName);
}
// BUG FIX: https://savannah.cern.ch/bugs/?83317
// use or not secondary groups in lease filename
if (useSecondaryGroupNamesForMapping_ && secondaryGroupNames != null
&& !secondaryGroupNames.isEmpty()) {
for (String secondaryGroupName : secondaryGroupNames) {
identifier.append(":").append(secondaryGroupName);
}
}
return identifier.toString();
}
/**
* Alpha numeric characters set: <code>[0-9a-zA-Z]</code>
*/
protected static final BitSet ALPHANUM= new BitSet(256);
// Static initializer for alphanum
static {
for (int i= 'a'; i <= 'z'; i++) {
ALPHANUM.set(i);
}
for (int i= 'A'; i <= 'Z'; i++) {
ALPHANUM.set(i);
}
for (int i= '0'; i <= '9'; i++) {
ALPHANUM.set(i);
}
}
/**
* Encodes the unescaped subject identifier, typically the user DN.
* <p>
* Implements the legacy string encoding used by gLExec LCAS/LCMAP for the
* lease file names:
* <ul>
* <li>URL encode all no alpha-numeric characters <code>[0-9a-zA-Z]</code>
* <li>apply lower case
* </ul>
*
* @param unescaped
* The unescaped user DN
* @return encoded, escaped, user DN, compatible with gLExec
* @throws URIException
*/
protected String encodeSubjectIdentifier(String unescaped)
throws URIException {
String encoded= URIUtil.encode(unescaped, ALPHANUM);
return encoded.toLowerCase();
}
/**
* Builds the absolute path to the subject identifier file.
*
* @param subjectIdentifier
* the subject identifier
*
* @return the absolute path to the subject identifier file
*/
protected String buildSubjectIdentifierFilePath(String subjectIdentifier) {
return gridMapDirectory_.getAbsolutePath() + File.separator
+ subjectIdentifier;
}
/**
* Gets a list of account files where the file names begin with the given
* prefix.
* <ul>
* <li>BUG FIX: https://savannah.cern.ch/bugs/?66574
* </ul>
*
* @param prefix
* prefix with which the file names should begin, may be null to
* signify all file names
*
* @return the selected account files
*/
private File[] getAccountFiles(final String prefix) {
return gridMapDirectory_.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
Matcher nameMatcher= poolAccountNamePattern_.matcher(name);
if (nameMatcher.matches()) {
// BUG FIX: https://savannah.cern.ch/bugs/?66574
if (prefix == null || prefix.equals(nameMatcher.group(1))) {
return true;
}
}
return false;
}
});
}
/**
* Gets a list of account file names where the names begin with the given
* prefix.
* <ul>
* <li>BUG FIX: https://savannah.cern.ch/bugs/?66574
* </ul>
*
* @param prefix
* prefix with which the file names should begin, may be null to
* signify all file names
*
* @return the selected account file names
*/
private String[] getAccountFileNames(final String prefix) {
return gridMapDirectory_.list(new FilenameFilter() {
public boolean accept(File dir, String name) {
Matcher nameMatcher= poolAccountNamePattern_.matcher(name);
if (nameMatcher.matches()) {
// BUG FIX: https://savannah.cern.ch/bugs/?66574
if (prefix == null || prefix.equals(nameMatcher.group(1))) {
return true;
}
}
return false;
}
});
}
/**
* @param useSecondaryGroupNamesForMapping
* the useSecondaryGroupNamesForMapping_ to set
*/
protected void setUseSecondaryGroupNamesForMapping(
boolean useSecondaryGroupNamesForMapping) {
this.useSecondaryGroupNamesForMapping_= useSecondaryGroupNamesForMapping;
}
}