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.util.Arrays;
024import java.util.HashMap;
025import java.util.Map;
026import java.util.concurrent.atomic.AtomicReference;
027import javax.security.auth.callback.Callback;
028import javax.security.auth.callback.CallbackHandler;
029import javax.security.auth.callback.NameCallback;
030import javax.security.auth.callback.PasswordCallback;
031import javax.security.auth.callback.UnsupportedCallbackException;
032import javax.security.sasl.AuthorizeCallback;
033import javax.security.sasl.RealmCallback;
034import org.apache.hadoop.conf.Configuration;
035import org.apache.hadoop.fs.FSDataInputStream;
036import org.apache.hadoop.fs.FileSystem;
037import org.apache.hadoop.fs.Path;
038import org.apache.hadoop.hbase.security.provider.AttemptingUserProvidingSaslServer;
039import org.apache.hadoop.hbase.security.provider.SaslServerAuthenticationProvider;
040import org.apache.hadoop.security.UserGroupInformation;
041import org.apache.hadoop.security.token.SecretManager;
042import org.apache.hadoop.security.token.SecretManager.InvalidToken;
043import org.apache.hadoop.security.token.TokenIdentifier;
044import org.apache.hadoop.util.StringUtils;
045import org.apache.yetus.audience.InterfaceAudience;
046import org.slf4j.Logger;
047import org.slf4j.LoggerFactory;
048
049@InterfaceAudience.Private
050public class ShadeSaslServerAuthenticationProvider extends ShadeSaslAuthenticationProvider
051  implements SaslServerAuthenticationProvider {
052  private static final Logger LOG =
053    LoggerFactory.getLogger(ShadeSaslServerAuthenticationProvider.class);
054
055  public static final String PASSWORD_FILE_KEY = "hbase.security.shade.password.file";
056  static final char SEPARATOR = '=';
057
058  private AtomicReference<UserGroupInformation> attemptingUser = new AtomicReference<>(null);
059  private Map<String, char[]> passwordDatabase;
060
061  @Override
062  public void init(Configuration conf) throws IOException {
063    passwordDatabase = readPasswordDB(conf);
064  }
065
066  @Override
067  public AttemptingUserProvidingSaslServer
068    createServer(SecretManager<TokenIdentifier> secretManager, Map<String, String> saslProps)
069      throws IOException {
070    return new AttemptingUserProvidingSaslServer(
071      new SaslPlainServer(new ShadeSaslServerCallbackHandler(attemptingUser, passwordDatabase)),
072      () -> attemptingUser.get());
073  }
074
075  Map<String, char[]> readPasswordDB(Configuration conf) throws IOException {
076    String passwordFileName = conf.get(PASSWORD_FILE_KEY);
077    if (passwordFileName == null) {
078      throw new RuntimeException(
079        PASSWORD_FILE_KEY + " is not defined in configuration, cannot use this implementation");
080    }
081
082    Path passwordFile = new Path(passwordFileName);
083    FileSystem fs = passwordFile.getFileSystem(conf);
084    if (!fs.exists(passwordFile)) {
085      throw new RuntimeException("Configured password file does not exist: " + passwordFile);
086    }
087
088    Map<String, char[]> passwordDb = new HashMap<>();
089    try (FSDataInputStream fdis = fs.open(passwordFile);
090      BufferedReader reader = new BufferedReader(new InputStreamReader(fdis))) {
091      String line = null;
092      int offset = 0;
093      while ((line = reader.readLine()) != null) {
094        line = line.trim();
095        String[] parts = StringUtils.split(line, SEPARATOR);
096        if (parts.length < 2) {
097          LOG.warn("Password file contains invalid record on line {}, skipping", offset + 1);
098          continue;
099        }
100
101        final String username = parts[0];
102        StringBuilder builder = new StringBuilder();
103        for (int i = 1; i < parts.length; i++) {
104          if (builder.length() > 0) {
105            builder.append(SEPARATOR);
106          }
107          builder.append(parts[i]);
108        }
109
110        passwordDb.put(username, builder.toString().toCharArray());
111        offset++;
112      }
113    }
114
115    return passwordDb;
116  }
117
118  @Override
119  public boolean supportsProtocolAuthentication() {
120    return false;
121  }
122
123  @Override
124  public UserGroupInformation getAuthorizedUgi(String authzId,
125    SecretManager<TokenIdentifier> secretManager) throws IOException {
126    return UserGroupInformation.createRemoteUser(authzId);
127  }
128
129  static class ShadeSaslServerCallbackHandler implements CallbackHandler {
130    private final AtomicReference<UserGroupInformation> attemptingUser;
131    private final Map<String, char[]> passwordDatabase;
132
133    public ShadeSaslServerCallbackHandler(AtomicReference<UserGroupInformation> attemptingUser,
134      Map<String, char[]> passwordDatabase) {
135      this.attemptingUser = attemptingUser;
136      this.passwordDatabase = passwordDatabase;
137    }
138
139    @Override
140    public void handle(Callback[] callbacks) throws InvalidToken, UnsupportedCallbackException {
141      NameCallback nc = null;
142      PasswordCallback pc = null;
143      AuthorizeCallback ac = null;
144      for (Callback callback : callbacks) {
145        if (callback instanceof AuthorizeCallback) {
146          ac = (AuthorizeCallback) callback;
147        } else if (callback instanceof NameCallback) {
148          nc = (NameCallback) callback;
149        } else if (callback instanceof PasswordCallback) {
150          pc = (PasswordCallback) callback;
151        } else if (callback instanceof RealmCallback) {
152          continue; // realm is ignored
153        } else {
154          throw new UnsupportedCallbackException(callback, "Unrecognized SASL PLAIN Callback");
155        }
156      }
157
158      if (nc != null && pc != null) {
159        String username = nc.getName();
160
161        UserGroupInformation ugi = createUgiForRemoteUser(username);
162        attemptingUser.set(ugi);
163
164        char[] clientPassword = pc.getPassword();
165        char[] actualPassword = passwordDatabase.get(username);
166        if (!Arrays.equals(clientPassword, actualPassword)) {
167          throw new InvalidToken("Authentication failed for " + username);
168        }
169      }
170
171      if (ac != null) {
172        String authenticatedUserId = ac.getAuthenticationID();
173        String userRequestedToExecuteAs = ac.getAuthorizationID();
174        if (authenticatedUserId.equals(userRequestedToExecuteAs)) {
175          ac.setAuthorized(true);
176          ac.setAuthorizedID(userRequestedToExecuteAs);
177        } else {
178          ac.setAuthorized(false);
179        }
180      }
181    }
182
183    UserGroupInformation createUgiForRemoteUser(String username) {
184      UserGroupInformation ugi = UserGroupInformation.createRemoteUser(username);
185      ugi.setAuthenticationMethod(ShadeSaslAuthenticationProvider.METHOD.getAuthMethod());
186      return ugi;
187    }
188  }
189}