From ba86c18f20962f31a90bfecc89d4fdba93dc39d9 Mon Sep 17 00:00:00 2001 From: Srikanth Srungarapu Date: Thu, 11 Dec 2014 19:13:53 -0800 Subject: [PATCH] HBASE-12704 Add demo client which uses doAs functionality on Thrift-over-HTTPS --- .../hadoop/hbase/thrift/HttpDoAsClient.java | 290 ++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 hbase-examples/src/main/java/org/apache/hadoop/hbase/thrift/HttpDoAsClient.java diff --git a/hbase-examples/src/main/java/org/apache/hadoop/hbase/thrift/HttpDoAsClient.java b/hbase-examples/src/main/java/org/apache/hadoop/hbase/thrift/HttpDoAsClient.java new file mode 100644 index 00000000000..9da79acb76c --- /dev/null +++ b/hbase-examples/src/main/java/org/apache/hadoop/hbase/thrift/HttpDoAsClient.java @@ -0,0 +1,290 @@ +/** + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.thrift; + +import sun.misc.BASE64Encoder; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.security.auth.Subject; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; + +import org.apache.hadoop.hbase.thrift.generated.AlreadyExists; +import org.apache.hadoop.hbase.thrift.generated.ColumnDescriptor; +import org.apache.hadoop.hbase.thrift.generated.Hbase; +import org.apache.hadoop.hbase.thrift.generated.TCell; +import org.apache.hadoop.hbase.thrift.generated.TRowResult; +import org.apache.thrift.protocol.TBinaryProtocol; +import org.apache.thrift.protocol.TProtocol; +import org.apache.thrift.transport.THttpClient; +import org.apache.thrift.transport.TSocket; +import org.apache.thrift.transport.TTransport; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; + +/** + * See the instructions under hbase-examples/README.txt + */ +public class HttpDoAsClient { + + static protected int port; + static protected String host; + CharsetDecoder decoder = null; + private static boolean secure = false; + + public static void main(String[] args) throws Exception { + + if (args.length < 2 || args.length > 3) { + + System.out.println("Invalid arguments!"); + System.out.println("Usage: DemoClient host port [secure=false]"); + + System.exit(-1); + } + + port = Integer.parseInt(args[1]); + host = args[0]; + if (args.length > 2) { + secure = Boolean.parseBoolean(args[2]); + } + + final HttpDoAsClient client = new HttpDoAsClient(); + Subject.doAs(getSubject(), + new PrivilegedExceptionAction() { + @Override + public Void run() throws Exception { + client.run(); + return null; + } + }); + } + + HttpDoAsClient() { + decoder = Charset.forName("UTF-8").newDecoder(); + } + + // Helper to translate byte[]'s to UTF8 strings + private String utf8(byte[] buf) { + try { + return decoder.decode(ByteBuffer.wrap(buf)).toString(); + } catch (CharacterCodingException e) { + return "[INVALID UTF-8]"; + } + } + + // Helper to translate strings to UTF8 bytes + private byte[] bytes(String s) { + try { + return s.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + return null; + } + } + + private void run() throws Exception { + TTransport transport = new TSocket(host, port); + + transport.open(); + String url = "http://" + host + ":" + port; + THttpClient httpClient = new THttpClient(url); + httpClient.open(); + TProtocol protocol = new TBinaryProtocol(httpClient); + Hbase.Client client = new Hbase.Client(protocol); + + byte[] t = bytes("demo_table"); + + // + // Scan all tables, look for the demo table and delete it. + // + System.out.println("scanning tables..."); + for (ByteBuffer name : refresh(client, httpClient).getTableNames()) { + System.out.println(" found: " + utf8(name.array())); + if (utf8(name.array()).equals(utf8(t))) { + if (client.isTableEnabled(name)) { + System.out.println(" disabling table: " + utf8(name.array())); + refresh(client, httpClient).disableTable(name); + } + System.out.println(" deleting table: " + utf8(name.array())); + refresh(client, httpClient).deleteTable(name); + } + } + + + + // + // Create the demo table with two column families, entry: and unused: + // + ArrayList columns = new ArrayList(); + ColumnDescriptor col; + col = new ColumnDescriptor(); + col.name = ByteBuffer.wrap(bytes("entry:")); + col.timeToLive = Integer.MAX_VALUE; + col.maxVersions = 10; + columns.add(col); + col = new ColumnDescriptor(); + col.name = ByteBuffer.wrap(bytes("unused:")); + col.timeToLive = Integer.MAX_VALUE; + columns.add(col); + + System.out.println("creating table: " + utf8(t)); + try { + + refresh(client, httpClient).createTable(ByteBuffer.wrap(t), columns); + } catch (AlreadyExists ae) { + System.out.println("WARN: " + ae.message); + } + + System.out.println("column families in " + utf8(t) + ": "); + Map columnMap = refresh(client, httpClient) + .getColumnDescriptors(ByteBuffer.wrap(t)); + for (ColumnDescriptor col2 : columnMap.values()) { + System.out.println(" column: " + utf8(col2.name.array()) + ", maxVer: " + Integer.toString(col2.maxVersions)); + } + + transport.close(); + httpClient.close(); + } + + private Hbase.Client refresh(Hbase.Client client, THttpClient httpClient) { + if(secure) { + httpClient.setCustomHeader("doAs", "hbase"); + try { + httpClient.setCustomHeader("Authorization", generateTicket()); + } catch (GSSException e) { + e.printStackTrace(); + } + } + return client; + } + + private String generateTicket() throws GSSException { + final GSSManager manager = GSSManager.getInstance(); + // Oid for kerberos principal name + Oid krb5PrincipalOid = new Oid("1.2.840.113554.1.2.2.1"); + Oid KERB_V5_OID = new Oid("1.2.840.113554.1.2.2"); + final GSSName clientName = manager.createName("hbase/node-1.internal@INTERNAL", + krb5PrincipalOid); + final GSSCredential clientCred = manager.createCredential(clientName, + 8 * 3600, + KERB_V5_OID, + GSSCredential.INITIATE_ONLY); + + final GSSName serverName = manager.createName("hbase/node-1.internal@INTERNAL", krb5PrincipalOid); + + final GSSContext context = manager.createContext(serverName, + KERB_V5_OID, + clientCred, + GSSContext.DEFAULT_LIFETIME); + context.requestMutualAuth(true); + context.requestConf(false); + context.requestInteg(true); + + final byte[] outToken = context.initSecContext(new byte[0], 0, 0); + StringBuffer outputBuffer = new StringBuffer(); + outputBuffer.append("Negotiate "); + outputBuffer.append(new BASE64Encoder().encode(outToken).replace("\n", "")); + System.out.print("Ticket is: " + outputBuffer); + return outputBuffer.toString(); + } + + private void printVersions(ByteBuffer row, List versions) { + StringBuilder rowStr = new StringBuilder(); + for (TCell cell : versions) { + rowStr.append(utf8(cell.value.array())); + rowStr.append("; "); + } + System.out.println("row: " + utf8(row.array()) + ", values: " + rowStr); + } + + private void printRow(TRowResult rowResult) { + // copy values into a TreeMap to get them in sorted order + + TreeMap sorted = new TreeMap(); + for (Map.Entry column : rowResult.columns.entrySet()) { + sorted.put(utf8(column.getKey().array()), column.getValue()); + } + + StringBuilder rowStr = new StringBuilder(); + for (SortedMap.Entry entry : sorted.entrySet()) { + rowStr.append(entry.getKey()); + rowStr.append(" => "); + rowStr.append(utf8(entry.getValue().value.array())); + rowStr.append("; "); + } + System.out.println("row: " + utf8(rowResult.row.array()) + ", cols: " + rowStr); + } + + private void printRow(List rows) { + for (TRowResult rowResult : rows) { + printRow(rowResult); + } + } + + static Subject getSubject() throws Exception { + if (!secure) return new Subject(); + /* + * To authenticate the DemoClient, kinit should be invoked ahead. + * Here we try to get the Kerberos credential from the ticket cache. + */ + LoginContext context = new LoginContext("", new Subject(), null, + new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map options = new HashMap(); + options.put("useKeyTab", "false"); + options.put("storeKey", "false"); + options.put("doNotPrompt", "true"); + options.put("useTicketCache", "true"); + options.put("renewTGT", "true"); + options.put("refreshKrb5Config", "true"); + options.put("isInitiator", "true"); + String ticketCache = System.getenv("KRB5CCNAME"); + if (ticketCache != null) { + options.put("ticketCache", ticketCache); + } + options.put("debug", "true"); + + return new AppConfigurationEntry[]{ + new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + options)}; + } + }); + context.login(); + return context.getSubject(); + } +}