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