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 */
018
019package org.apache.hadoop.hbase;
020
021import java.io.IOException;
022import java.net.UnknownHostException;
023
024import org.apache.hadoop.conf.Configuration;
025import org.apache.hadoop.hbase.security.UserProvider;
026import org.apache.hadoop.hbase.util.DNS;
027import org.apache.hadoop.hbase.util.Strings;
028import org.apache.hadoop.security.UserGroupInformation;
029import org.apache.yetus.audience.InterfaceAudience;
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032
033/**
034 * Utility methods for helping with security tasks. Downstream users
035 * may rely on this class to handle authenticating via keytab where
036 * long running services need access to a secure HBase cluster.
037 *
038 * Callers must ensure:
039 *
040 * <ul>
041 *   <li>HBase configuration files are in the Classpath
042 *   <li>hbase.client.keytab.file points to a valid keytab on the local filesystem
043 *   <li>hbase.client.kerberos.principal gives the Kerberos principal to use
044 * </ul>
045 *
046 * <pre>
047 * {@code
048 *   ChoreService choreService = null;
049 *   // Presumes HBase configuration files are on the classpath
050 *   final Configuration conf = HBaseConfiguration.create();
051 *   final ScheduledChore authChore = AuthUtil.getAuthChore(conf);
052 *   if (authChore != null) {
053 *     choreService = new ChoreService("MY_APPLICATION");
054 *     choreService.scheduleChore(authChore);
055 *   }
056 *   try {
057 *     // do application work
058 *   } finally {
059 *     if (choreService != null) {
060 *       choreService.shutdown();
061 *     }
062 *   }
063 * }
064 * </pre>
065 *
066 * See the "Running Canary in a Kerberos-enabled Cluster" section of the HBase Reference Guide for
067 * an example of configuring a user of this Auth Chore to run on a secure cluster.
068 */
069@InterfaceAudience.Public
070public class AuthUtil {
071  private static final Logger LOG = LoggerFactory.getLogger(AuthUtil.class);
072
073  /** Prefix character to denote group names */
074  private static final String GROUP_PREFIX = "@";
075
076  private AuthUtil() {
077    super();
078  }
079
080  /**
081   * Checks if security is enabled and if so, launches chore for refreshing kerberos ticket.
082   * @param conf the hbase service configuration
083   * @return a ScheduledChore for renewals, if needed, and null otherwise.
084   */
085  public static ScheduledChore getAuthChore(Configuration conf) throws IOException {
086    UserProvider userProvider = UserProvider.instantiate(conf);
087    // login the principal (if using secure Hadoop)
088    boolean securityEnabled =
089        userProvider.isHadoopSecurityEnabled() && userProvider.isHBaseSecurityEnabled();
090    if (!securityEnabled) return null;
091    String host = null;
092    try {
093      host = Strings.domainNamePointerToHostName(DNS.getDefaultHost(
094          conf.get("hbase.client.dns.interface", "default"),
095          conf.get("hbase.client.dns.nameserver", "default")));
096      userProvider.login("hbase.client.keytab.file", "hbase.client.kerberos.principal", host);
097    } catch (UnknownHostException e) {
098      LOG.error("Error resolving host name: " + e.getMessage(), e);
099      throw e;
100    } catch (IOException e) {
101      LOG.error("Error while trying to perform the initial login: " + e.getMessage(), e);
102      throw e;
103    }
104
105    final UserGroupInformation ugi = userProvider.getCurrent().getUGI();
106    Stoppable stoppable = new Stoppable() {
107      private volatile boolean isStopped = false;
108
109      @Override
110      public void stop(String why) {
111        isStopped = true;
112      }
113
114      @Override
115      public boolean isStopped() {
116        return isStopped;
117      }
118    };
119
120    // if you're in debug mode this is useful to avoid getting spammed by the getTGT()
121    // you can increase this, keeping in mind that the default refresh window is 0.8
122    // e.g. 5min tgt * 0.8 = 4min refresh so interval is better be way less than 1min
123    final int CHECK_TGT_INTERVAL = 30 * 1000; // 30sec
124
125    ScheduledChore refreshCredentials =
126        new ScheduledChore("RefreshCredentials", stoppable, CHECK_TGT_INTERVAL) {
127      @Override
128      protected void chore() {
129        try {
130          ugi.checkTGTAndReloginFromKeytab();
131        } catch (IOException e) {
132          LOG.error("Got exception while trying to refresh credentials: " + e.getMessage(), e);
133        }
134      }
135    };
136
137    return refreshCredentials;
138  }
139
140  /**
141   * Returns whether or not the given name should be interpreted as a group
142   * principal.  Currently this simply checks if the name starts with the
143   * special group prefix character ("@").
144   */
145  @InterfaceAudience.Private
146  public static boolean isGroupPrincipal(String name) {
147    return name != null && name.startsWith(GROUP_PREFIX);
148  }
149
150  /**
151   * Returns the actual name for a group principal (stripped of the
152   * group prefix).
153   */
154  @InterfaceAudience.Private
155  public static String getGroupName(String aclKey) {
156    if (!isGroupPrincipal(aclKey)) {
157      return aclKey;
158    }
159
160    return aclKey.substring(GROUP_PREFIX.length());
161  }
162
163  /**
164   * Returns the group entry with the group prefix for a group principal.
165   */
166  @InterfaceAudience.Private
167  public static String toGroupEntry(String name) {
168    return GROUP_PREFIX + name;
169  }
170}