blob: c76eaf6f8672f1f8bca5bea688fbd7151f58cafd [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.drill.exec.rpc.user.security;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.codec.digest.Md5Crypt;
import org.apache.drill.common.config.DrillConfig;
import org.apache.drill.exec.ExecConstants;
import org.apache.drill.exec.exception.DrillbitStartupException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Implementation of UserAuthenticator that reads passwords from an htpasswd
* formatted file.
* <p>
* Currently supports MD5, SHA-1, and plaintext passwords.
* <p>
* Use the htpasswd command line tool to create and modify htpasswd files.
* <p>
* By default this loads the passwords from <code>/opt/drill/conf/htpasswd</code>. Users can change the path by
* putting the absolute file path as <code>drill.exec.security.user.auth.htpasswd.path</code> in
* <code>drill-override.conf</code>.
* <p>
* This is intended for situations where the list of users is relatively static, and you are running
* drill in a container so using pam is not convenient.
*/
@UserAuthenticatorTemplate(type = "htpasswd")
public class HtpasswdFileUserAuthenticator implements UserAuthenticator {
private static final Logger logger = LoggerFactory.getLogger(HtpasswdFileUserAuthenticator.class);
private static final Pattern HTPASSWD_LINE_PATTERN = Pattern.compile("^([^:]+):([^:]+)");
public static final String DEFAULT_HTPASSWD_AUTHENTICATOR_PATH = "/opt/drill/conf/htpasswd";
private String path = DEFAULT_HTPASSWD_AUTHENTICATOR_PATH;
private long lastModified;
private long lastFileSize;
private Map<String, String> userToPassword;
@Override
public void setup(DrillConfig drillConfig) throws DrillbitStartupException {
if (drillConfig.hasPath(ExecConstants.HTPASSWD_AUTHENTICATOR_PATH)) {
path = drillConfig.getString(ExecConstants.HTPASSWD_AUTHENTICATOR_PATH);
}
}
/**
* Check password against hash read from the file
*
* @param password User provided password
* @param hash Hash stored in the htpasswd file
* @return true if the password matched the hash
*/
public static boolean isPasswordValid(String password, String hash) {
if (hash.startsWith("$apr1$")) {
return hash.equals(Md5Crypt.apr1Crypt(password, hash));
} else if (hash.startsWith("$1$")) {
return hash.equals(Md5Crypt.md5Crypt(password.getBytes(StandardCharsets.UTF_8), hash));
} else if (hash.startsWith("{SHA}")) {
return hash.substring(5).equals(Base64.getEncoder().encodeToString(DigestUtils.sha1(password)));
} else if (hash.startsWith("$2y$")) {
// bcrypt not supported currently
return false;
} else {
return hash.equals(password);
}
}
/**
* Validate the given username and password against the password file
*
* @param username Username provided
* @param password Password provided
* @throws UserAuthenticationException If the username and password could not be validated
*/
@Override
public void authenticate(String username, String password) throws UserAuthenticationException {
read();
String hash = this.userToPassword.get(username);
boolean credentialsAccepted = (hash != null && isPasswordValid(password, hash));
if (!credentialsAccepted) {
throw new UserAuthenticationException(String.format("htpasswd auth failed for user '%s'",
username));
}
}
/**
* Read the password file into the map, if the file has changed since we last read it
*/
protected synchronized void read() {
File file = new File(path);
long newLastModified = file.exists() ? file.lastModified() : 0;
long newFileSize = file.exists() ? file.length() : 0;
if (userToPassword == null || newLastModified != lastModified || newFileSize != lastFileSize) {
HashMap<String, String> newMap = new HashMap<>();
if(newFileSize != 0) {
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
while ((line = reader.readLine()) != null) {
if (!line.isEmpty() && !line.startsWith("#")) {
Matcher m = HTPASSWD_LINE_PATTERN.matcher(line);
if (m.matches()) {
newMap.put(m.group(1), m.group(2));
}
}
}
} catch (Exception e) {
logger.error(MessageFormat.format("Failed to read htpasswd file at path {0}", file), e);
}
} else {
logger.error(MessageFormat.format("Empty or missing htpasswd file at path {0}", file));
}
lastFileSize = newFileSize;
lastModified = newLastModified;
userToPassword = newMap;
}
}
/**
* Free resources associated with this authenticator
*/
@Override
public void close() {
lastModified = 0;
userToPassword = null;
}
}