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