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}