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}