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}