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      NameCallback nc = null;
143      PasswordCallback pc = null;
144      AuthorizeCallback ac = null;
145      for (Callback callback : callbacks) {
146        if (callback instanceof AuthorizeCallback) {
147          ac = (AuthorizeCallback) callback;
148        } else if (callback instanceof NameCallback) {
149          nc = (NameCallback) callback;
150        } else if (callback instanceof PasswordCallback) {
151          pc = (PasswordCallback) callback;
152        } else if (callback instanceof RealmCallback) {
153          continue; // realm is ignored
154        } else {
155          throw new UnsupportedCallbackException(callback, "Unrecognized SASL PLAIN Callback");
156        }
157      }
158
159      if (nc != null && pc != null) {
160        String username = nc.getName();
161
162        UserGroupInformation ugi = createUgiForRemoteUser(username);
163        attemptingUser.set(ugi);
164
165        char[] clientPassword = pc.getPassword();
166        char[] actualPassword = passwordDatabase.get(username);
167        if (!Arrays.equals(clientPassword, actualPassword)) {
168          throw new InvalidToken("Authentication failed for " + username);
169        }
170      }
171
172      if (ac != null) {
173        String authenticatedUserId = ac.getAuthenticationID();
174        String userRequestedToExecuteAs = ac.getAuthorizationID();
175        if (authenticatedUserId.equals(userRequestedToExecuteAs)) {
176          ac.setAuthorized(true);
177          ac.setAuthorizedID(userRequestedToExecuteAs);
178        } else {
179          ac.setAuthorized(false);
180        }
181      }
182    }
183
184    UserGroupInformation createUgiForRemoteUser(String username) {
185      UserGroupInformation ugi = UserGroupInformation.createRemoteUser(username);
186      ugi.setAuthenticationMethod(ShadeSaslAuthenticationProvider.METHOD.getAuthMethod());
187      return ugi;
188    }
189  }
190}