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.thrift; 019 020import java.io.File; 021import java.nio.ByteBuffer; 022import java.security.Principal; 023import java.security.PrivilegedExceptionAction; 024import java.util.ArrayList; 025import java.util.Base64; 026import java.util.HashMap; 027import java.util.HashSet; 028import java.util.Map; 029import java.util.Set; 030import javax.security.auth.Subject; 031import javax.security.auth.kerberos.KerberosPrincipal; 032import javax.security.auth.login.AppConfigurationEntry; 033import javax.security.auth.login.Configuration; 034import javax.security.auth.login.LoginContext; 035import org.apache.hadoop.hbase.thrift.generated.AlreadyExists; 036import org.apache.hadoop.hbase.thrift.generated.ColumnDescriptor; 037import org.apache.hadoop.hbase.thrift.generated.Hbase; 038import org.apache.hadoop.hbase.util.Bytes; 039import org.apache.hadoop.hbase.util.ClientUtils; 040import org.apache.yetus.audience.InterfaceAudience; 041import org.ietf.jgss.GSSContext; 042import org.ietf.jgss.GSSCredential; 043import org.ietf.jgss.GSSException; 044import org.ietf.jgss.GSSManager; 045import org.ietf.jgss.GSSName; 046import org.ietf.jgss.Oid; 047import org.slf4j.Logger; 048import org.slf4j.LoggerFactory; 049 050import org.apache.hbase.thirdparty.org.apache.thrift.protocol.TBinaryProtocol; 051import org.apache.hbase.thirdparty.org.apache.thrift.protocol.TProtocol; 052import org.apache.hbase.thirdparty.org.apache.thrift.transport.THttpClient; 053import org.apache.hbase.thirdparty.org.apache.thrift.transport.TSocket; 054import org.apache.hbase.thirdparty.org.apache.thrift.transport.TTransport; 055 056/** 057 * See the instructions under hbase-examples/README.txt 058 */ 059@InterfaceAudience.Private 060public class HttpDoAsClient { 061 private static final Logger LOG = LoggerFactory.getLogger(HttpDoAsClient.class); 062 063 static protected int port; 064 static protected String host; 065 private static boolean secure = false; 066 static protected String doAsUser = null; 067 static protected String principal = null; 068 static protected String keyTab = null; 069 070 public static void main(String[] args) throws Exception { 071 if (args.length < 3 || args.length > 6) { 072 System.out.println("Invalid arguments!"); 073 System.out.println( 074 "Usage: HttpDoAsClient host port doAsUserName [security=true] [principal] [keytab]"); 075 System.exit(-1); 076 } 077 078 host = args[0]; 079 port = Integer.parseInt(args[1]); 080 doAsUser = args[2]; 081 if (args.length > 3) { 082 secure = Boolean.parseBoolean(args[3]); 083 if (args.length > 4) { 084 principal = args[4]; 085 keyTab = args[5]; 086 if (!new File(keyTab).exists()) { 087 System.err.printf("ERROR: KeyTab File %s not found %n", keyTab); 088 System.exit(-1); 089 } 090 } else { 091 principal = getSubject().getPrincipals().iterator().next().getName(); 092 } 093 } 094 095 final HttpDoAsClient client = new HttpDoAsClient(); 096 Subject.doAs(getSubject(), new PrivilegedExceptionAction<Void>() { 097 @Override 098 public Void run() throws Exception { 099 client.run(); 100 return null; 101 } 102 }); 103 } 104 105 HttpDoAsClient() { 106 } 107 108 // Helper to translate strings to UTF8 bytes 109 private byte[] bytes(String s) { 110 return Bytes.toBytes(s); 111 } 112 113 private void run() throws Exception { 114 TTransport transport = new TSocket(host, port); 115 116 transport.open(); 117 String url = "http://" + host + ":" + port; 118 THttpClient httpClient = new THttpClient(url); 119 httpClient.open(); 120 TProtocol protocol = new TBinaryProtocol(httpClient); 121 Hbase.Client client = new Hbase.Client(protocol); 122 123 byte[] t = bytes("demo_table"); 124 125 // 126 // Scan all tables, look for the demo table and delete it. 127 // 128 System.out.println("scanning tables..."); 129 for (ByteBuffer name : refresh(client, httpClient).getTableNames()) { 130 System.out.println(" found: " + ClientUtils.utf8(name)); 131 if (ClientUtils.utf8(name).equals(ClientUtils.utf8(t))) { 132 if (refresh(client, httpClient).isTableEnabled(name)) { 133 System.out.println(" disabling table: " + ClientUtils.utf8(name)); 134 refresh(client, httpClient).disableTable(name); 135 } 136 System.out.println(" deleting table: " + ClientUtils.utf8(name)); 137 refresh(client, httpClient).deleteTable(name); 138 } 139 } 140 141 // 142 // Create the demo table with two column families, entry: and unused: 143 // 144 ArrayList<ColumnDescriptor> columns = new ArrayList<>(2); 145 ColumnDescriptor col; 146 col = new ColumnDescriptor(); 147 col.name = ByteBuffer.wrap(bytes("entry:")); 148 col.timeToLive = Integer.MAX_VALUE; 149 col.maxVersions = 10; 150 columns.add(col); 151 col = new ColumnDescriptor(); 152 col.name = ByteBuffer.wrap(bytes("unused:")); 153 col.timeToLive = Integer.MAX_VALUE; 154 columns.add(col); 155 156 System.out.println("creating table: " + ClientUtils.utf8(t)); 157 try { 158 159 refresh(client, httpClient).createTable(ByteBuffer.wrap(t), columns); 160 } catch (AlreadyExists ae) { 161 System.out.println("WARN: " + ae.message); 162 } 163 164 System.out.println("column families in " + ClientUtils.utf8(t) + ": "); 165 Map<ByteBuffer, ColumnDescriptor> columnMap = 166 refresh(client, httpClient).getColumnDescriptors(ByteBuffer.wrap(t)); 167 for (ColumnDescriptor col2 : columnMap.values()) { 168 System.out 169 .println(" column: " + ClientUtils.utf8(col2.name) + ", maxVer: " + col2.maxVersions); 170 } 171 172 transport.close(); 173 httpClient.close(); 174 } 175 176 private Hbase.Client refresh(Hbase.Client client, THttpClient httpClient) { 177 httpClient.setCustomHeader("doAs", doAsUser); 178 if (secure) { 179 try { 180 httpClient.setCustomHeader("Authorization", generateTicket()); 181 } catch (GSSException e) { 182 LOG.error("Kerberos authentication failed", e); 183 } 184 } 185 return client; 186 } 187 188 private String generateTicket() throws GSSException { 189 final GSSManager manager = GSSManager.getInstance(); 190 // Oid for kerberos principal name 191 Oid krb5PrincipalOid = new Oid("1.2.840.113554.1.2.2.1"); 192 Oid KERB_V5_OID = new Oid("1.2.840.113554.1.2.2"); 193 final GSSName clientName = manager.createName(principal, krb5PrincipalOid); 194 final GSSCredential clientCred = 195 manager.createCredential(clientName, 8 * 3600, KERB_V5_OID, GSSCredential.INITIATE_ONLY); 196 197 final GSSName serverName = manager.createName(principal, krb5PrincipalOid); 198 199 final GSSContext context = 200 manager.createContext(serverName, KERB_V5_OID, clientCred, GSSContext.DEFAULT_LIFETIME); 201 context.requestMutualAuth(true); 202 context.requestConf(false); 203 context.requestInteg(true); 204 205 final byte[] outToken = context.initSecContext(new byte[0], 0, 0); 206 StringBuilder outputBuffer = new StringBuilder(); 207 outputBuffer.append("Negotiate "); 208 outputBuffer.append(Bytes.toString(Base64.getEncoder().encode(outToken))); 209 System.out.print("Ticket is: " + outputBuffer); 210 return outputBuffer.toString(); 211 } 212 213 static Subject getSubject() throws Exception { 214 if (!secure) { 215 return new Subject(); 216 } 217 218 /* 219 * To authenticate the DemoClient, kinit should be invoked ahead. Here we try to get the 220 * Kerberos credential from the ticket cache. 221 */ 222 LoginContext context; 223 224 if (keyTab != null) { 225 // To authenticate the HttpDoAsClient using principal and keyTab 226 Set<Principal> principals = new HashSet<>(); 227 principals.add(new KerberosPrincipal(principal)); 228 Subject subject = new Subject(false, principals, new HashSet<>(), new HashSet<>()); 229 230 context = new LoginContext("", subject, null, new KerberosConfiguration(principal, keyTab)); 231 } else { 232 /* 233 * To authenticate the HttpDoAsClient, kinit should be invoked ahead. Here we try to get the 234 * Kerberos credential from the ticket cache. 235 */ 236 context = new LoginContext("", new Subject(), null, new KerberosConfiguration()); 237 } 238 context.login(); 239 return context.getSubject(); 240 } 241 242 private static class KerberosConfiguration extends Configuration { 243 private String principal; 244 private String keyTab; 245 246 public KerberosConfiguration() { 247 // Empty constructor will have no principal or keyTab values 248 } 249 250 public KerberosConfiguration(String principal, String keyTab) { 251 this.principal = principal; 252 this.keyTab = keyTab; 253 } 254 255 @Override 256 public AppConfigurationEntry[] getAppConfigurationEntry(String name) { 257 Map<String, String> options = new HashMap<>(); 258 if (principal != null && keyTab != null) { 259 options.put("principal", principal); 260 options.put("keyTab", keyTab); 261 options.put("useKeyTab", "true"); 262 options.put("storeKey", "true"); 263 } else { 264 options.put("useKeyTab", "false"); 265 options.put("storeKey", "false"); 266 } 267 options.put("doNotPrompt", "true"); 268 options.put("useTicketCache", "true"); 269 options.put("renewTGT", "true"); 270 options.put("refreshKrb5Config", "true"); 271 options.put("isInitiator", "true"); 272 String ticketCache = System.getenv("KRB5CCNAME"); 273 if (ticketCache != null) { 274 options.put("ticketCache", ticketCache); 275 } 276 options.put("debug", "true"); 277 278 return new AppConfigurationEntry[] { 279 new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", 280 AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options) }; 281 } 282 } 283}