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}