001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.security.provider.example;
019
020import java.io.BufferedReader;
021import java.io.IOException;
022import java.io.InputStreamReader;
023import java.nio.charset.StandardCharsets;
024import java.util.Arrays;
025import java.util.HashMap;
026import java.util.Map;
027import java.util.concurrent.atomic.AtomicReference;
028import javax.security.auth.callback.Callback;
029import javax.security.auth.callback.CallbackHandler;
030import javax.security.auth.callback.NameCallback;
031import javax.security.auth.callback.PasswordCallback;
032import javax.security.auth.callback.UnsupportedCallbackException;
033import javax.security.sasl.AuthorizeCallback;
034import javax.security.sasl.RealmCallback;
035import org.apache.hadoop.conf.Configuration;
036import org.apache.hadoop.fs.FSDataInputStream;
037import org.apache.hadoop.fs.FileSystem;
038import org.apache.hadoop.fs.Path;
039import org.apache.hadoop.hbase.security.provider.AttemptingUserProvidingSaslServer;
040import org.apache.hadoop.hbase.security.provider.SaslServerAuthenticationProvider;
041import org.apache.hadoop.security.UserGroupInformation;
042import org.apache.hadoop.security.token.SecretManager;
043import org.apache.hadoop.security.token.SecretManager.InvalidToken;
044import org.apache.hadoop.security.token.TokenIdentifier;
045import org.apache.hadoop.util.StringUtils;
046import org.apache.yetus.audience.InterfaceAudience;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050@InterfaceAudience.Private
051public class ShadeSaslServerAuthenticationProvider extends ShadeSaslAuthenticationProvider
052  implements SaslServerAuthenticationProvider {
053  private static final Logger LOG =
054    LoggerFactory.getLogger(ShadeSaslServerAuthenticationProvider.class);
055
056  public static final String PASSWORD_FILE_KEY = "hbase.security.shade.password.file";
057  static final char SEPARATOR = '=';
058
059  private AtomicReference<UserGroupInformation> attemptingUser = new AtomicReference<>(null);
060  private Map<String, char[]> passwordDatabase;
061
062  @Override
063  public void init(Configuration conf) throws IOException {
064    passwordDatabase = readPasswordDB(conf);
065  }
066
067  @Override
068  public AttemptingUserProvidingSaslServer
069    createServer(SecretManager<TokenIdentifier> secretManager, Map<String, String> saslProps)
070      throws IOException {
071    return new AttemptingUserProvidingSaslServer(
072      new SaslPlainServer(new ShadeSaslServerCallbackHandler(attemptingUser, passwordDatabase)),
073      () -> attemptingUser.get());
074  }
075
076  Map<String, char[]> readPasswordDB(Configuration conf) throws IOException {
077    String passwordFileName = conf.get(PASSWORD_FILE_KEY);
078    if (passwordFileName == null) {
079      throw new RuntimeException(
080        PASSWORD_FILE_KEY + " is not defined in configuration, cannot use this implementation");
081    }
082
083    Path passwordFile = new Path(passwordFileName);
084    FileSystem fs = passwordFile.getFileSystem(conf);
085    if (!fs.exists(passwordFile)) {
086      throw new RuntimeException("Configured password file does not exist: " + passwordFile);
087    }
088
089    Map<String, char[]> passwordDb = new HashMap<>();
090    try (FSDataInputStream fdis = fs.open(passwordFile); BufferedReader reader =
091      new BufferedReader(new InputStreamReader(fdis, StandardCharsets.UTF_8))) {
092      String line = null;
093      int offset = 0;
094      while ((line = reader.readLine()) != null) {
095        line = line.trim();
096        String[] parts = StringUtils.split(line, SEPARATOR);
097        if (parts.length < 2) {
098          LOG.warn("Password file contains invalid record on line {}, skipping", offset + 1);
099          continue;
100        }
101
102        final String username = parts[0];
103        StringBuilder builder = new StringBuilder();
104        for (int i = 1; i < parts.length; i++) {
105          if (builder.length() > 0) {
106            builder.append(SEPARATOR);
107          }
108          builder.append(parts[i]);
109        }
110
111        passwordDb.put(username, builder.toString().toCharArray());
112        offset++;
113      }
114    }
115
116    return passwordDb;
117  }
118
119  @Override
120  public boolean supportsProtocolAuthentication() {
121    return false;
122  }
123
124  @Override
125  public UserGroupInformation getAuthorizedUgi(String authzId,
126    SecretManager<TokenIdentifier> secretManager) throws IOException {
127    return UserGroupInformation.createRemoteUser(authzId);
128  }
129
130  static class ShadeSaslServerCallbackHandler implements CallbackHandler {
131    private final AtomicReference<UserGroupInformation> attemptingUser;
132    private final Map<String, char[]> passwordDatabase;
133
134    public ShadeSaslServerCallbackHandler(AtomicReference<UserGroupInformation> attemptingUser,
135      Map<String, char[]> passwordDatabase) {
136      this.attemptingUser = attemptingUser;
137      this.passwordDatabase = passwordDatabase;
138    }
139
140    @Override
141    public void handle(Callback[] callbacks) throws InvalidToken, UnsupportedCallbackException {
142      LOG.info("SaslServerCallbackHandler called", new Exception());
143      NameCallback nc = null;
144      PasswordCallback pc = null;
145      AuthorizeCallback ac = null;
146      for (Callback callback : callbacks) {
147        if (callback instanceof AuthorizeCallback) {
148          ac = (AuthorizeCallback) callback;
149        } else if (callback instanceof NameCallback) {
150          nc = (NameCallback) callback;
151        } else if (callback instanceof PasswordCallback) {
152          pc = (PasswordCallback) callback;
153        } else if (callback instanceof RealmCallback) {
154          continue; // realm is ignored
155        } else {
156          throw new UnsupportedCallbackException(callback, "Unrecognized SASL PLAIN Callback");
157        }
158      }
159
160      if (nc != null && pc != null) {
161        String username = nc.getName();
162
163        UserGroupInformation ugi = createUgiForRemoteUser(username);
164        attemptingUser.set(ugi);
165
166        char[] clientPassword = pc.getPassword();
167        char[] actualPassword = passwordDatabase.get(username);
168        if (!Arrays.equals(clientPassword, actualPassword)) {
169          throw new InvalidToken("Authentication failed for " + username);
170        }
171      }
172
173      if (ac != null) {
174        String authenticatedUserId = ac.getAuthenticationID();
175        String userRequestedToExecuteAs = ac.getAuthorizationID();
176        if (authenticatedUserId.equals(userRequestedToExecuteAs)) {
177          ac.setAuthorized(true);
178          ac.setAuthorizedID(userRequestedToExecuteAs);
179        } else {
180          ac.setAuthorized(false);
181        }
182      }
183    }
184
185    UserGroupInformation createUgiForRemoteUser(String username) {
186      UserGroupInformation ugi = UserGroupInformation.createRemoteUser(username);
187      ugi.setAuthenticationMethod(ShadeSaslAuthenticationProvider.METHOD.getAuthMethod());
188      return ugi;
189    }
190  }
191}