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