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;
019
020import java.io.IOException;
021import java.net.UnknownHostException;
022import org.apache.hadoop.conf.Configuration;
023import org.apache.hadoop.hbase.security.User;
024import org.apache.hadoop.hbase.security.UserProvider;
025import org.apache.hadoop.hbase.util.DNS;
026import org.apache.hadoop.hbase.util.Strings;
027import org.apache.hadoop.security.UserGroupInformation;
028import org.apache.yetus.audience.InterfaceAudience;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032/**
033 * Utility methods for helping with security tasks. Downstream users may rely on this class to
034 * handle authenticating via keytab where long running services need access to a secure HBase
035 * cluster. Callers must ensure:
036 * <ul>
037 * <li>HBase configuration files are in the Classpath
038 * <li>hbase.client.keytab.file points to a valid keytab on the local filesystem
039 * <li>hbase.client.kerberos.principal gives the Kerberos principal to use
040 * </ul>
041 *
042 * <pre>
043 * {
044 *   &#64;code
045 *   ChoreService choreService = null;
046 *   // Presumes HBase configuration files are on the classpath
047 *   final Configuration conf = HBaseConfiguration.create();
048 *   final ScheduledChore authChore = AuthUtil.getAuthChore(conf);
049 *   if (authChore != null) {
050 *     choreService = new ChoreService("MY_APPLICATION");
051 *     choreService.scheduleChore(authChore);
052 *   }
053 *   try {
054 *     // do application work
055 *   } finally {
056 *     if (choreService != null) {
057 *       choreService.shutdown();
058 *     }
059 *   }
060 * }
061 * </pre>
062 *
063 * See the "Running Canary in a Kerberos-enabled Cluster" section of the HBase Reference Guide for
064 * an example of configuring a user of this Auth Chore to run on a secure cluster.
065 *
066 * <pre>
067 * </pre>
068 *
069 * This class will be internal used only from 2.2.0 version, and will transparently work for
070 * kerberized applications. For more, please refer
071 * <a href="http://hbase.apache.org/book.html#hbase.secure.configuration">Client-side Configuration
072 * for Secure Operation</a>
073 * @deprecated since 2.2.0, to be marked as
074 *             {@link org.apache.yetus.audience.InterfaceAudience.Private} in 4.0.0.
075 * @see <a href="https://issues.apache.org/jira/browse/HBASE-20886">HBASE-20886</a>
076 */
077@Deprecated
078@InterfaceAudience.Public
079public final class AuthUtil {
080  private static final Logger LOG = LoggerFactory.getLogger(AuthUtil.class);
081
082  /** Prefix character to denote group names */
083  private static final String GROUP_PREFIX = "@";
084
085  /** Client keytab file */
086  public static final String HBASE_CLIENT_KEYTAB_FILE = "hbase.client.keytab.file";
087
088  /** Client principal */
089  public static final String HBASE_CLIENT_KERBEROS_PRINCIPAL = "hbase.client.keytab.principal";
090
091  /** Configuration to automatically try to renew keytab-based logins */
092  public static final String HBASE_CLIENT_AUTOMATIC_KEYTAB_RENEWAL_KEY =
093    "hbase.client.keytab.automatic.renewal";
094  public static final boolean HBASE_CLIENT_AUTOMATIC_KEYTAB_RENEWAL_DEFAULT = true;
095
096  private AuthUtil() {
097    super();
098  }
099
100  /**
101   * For kerberized cluster, return login user (from kinit or from keytab if specified). For
102   * non-kerberized cluster, return system user.
103   * @param conf configuartion file
104   * @throws IOException login exception
105   */
106  @InterfaceAudience.Private
107  public static User loginClient(Configuration conf) throws IOException {
108    UserProvider provider = UserProvider.instantiate(conf);
109    User user = provider.getCurrent();
110    boolean securityOn = provider.isHBaseSecurityEnabled() && provider.isHadoopSecurityEnabled();
111
112    if (securityOn) {
113      boolean fromKeytab = provider.shouldLoginFromKeytab();
114      if (user.getUGI().hasKerberosCredentials()) {
115        // There's already a login user.
116        // But we should avoid misuse credentials which is a dangerous security issue,
117        // so here check whether user specified a keytab and a principal:
118        // 1. Yes, check if user principal match.
119        // a. match, just return.
120        // b. mismatch, login using keytab.
121        // 2. No, user may login through kinit, this is the old way, also just return.
122        if (fromKeytab) {
123          return checkPrincipalMatch(conf, user.getUGI().getUserName())
124            ? user
125            : loginFromKeytabAndReturnUser(provider);
126        }
127        return user;
128      } else if (fromKeytab) {
129        // Kerberos is on and client specify a keytab and principal, but client doesn't login yet.
130        return loginFromKeytabAndReturnUser(provider);
131      }
132    }
133    return user;
134  }
135
136  private static boolean checkPrincipalMatch(Configuration conf, String loginUserName) {
137    String configuredUserName = conf.get(HBASE_CLIENT_KERBEROS_PRINCIPAL);
138    boolean match = configuredUserName.equals(loginUserName);
139    if (!match) {
140      LOG.warn("Trying to login with a different user: {}, existed user is {}.", configuredUserName,
141        loginUserName);
142    }
143    return match;
144  }
145
146  private static User loginFromKeytabAndReturnUser(UserProvider provider) throws IOException {
147    try {
148      provider.login(HBASE_CLIENT_KEYTAB_FILE, HBASE_CLIENT_KERBEROS_PRINCIPAL);
149    } catch (IOException ioe) {
150      LOG.error("Error while trying to login as user {} through {}, with message: {}.",
151        HBASE_CLIENT_KERBEROS_PRINCIPAL, HBASE_CLIENT_KEYTAB_FILE, ioe.getMessage());
152      throw ioe;
153    }
154    return provider.getCurrent();
155  }
156
157  /**
158   * For kerberized cluster, return login user (from kinit or from keytab). Principal should be the
159   * following format: name/fully.qualified.domain.name@REALM. For non-kerberized cluster, return
160   * system user.
161   * <p>
162   * NOT recommend to use to method unless you're sure what you're doing, it is for canary only.
163   * Please use User#loginClient.
164   * @param conf configuration file
165   * @throws IOException login exception
166   */
167  private static User loginClientAsService(Configuration conf) throws IOException {
168    UserProvider provider = UserProvider.instantiate(conf);
169    if (provider.isHBaseSecurityEnabled() && provider.isHadoopSecurityEnabled()) {
170      try {
171        if (provider.shouldLoginFromKeytab()) {
172          String host = Strings.domainNamePointerToHostName(
173            DNS.getDefaultHost(conf.get("hbase.client.dns.interface", "default"),
174              conf.get("hbase.client.dns.nameserver", "default")));
175          provider.login(HBASE_CLIENT_KEYTAB_FILE, HBASE_CLIENT_KERBEROS_PRINCIPAL, host);
176        }
177      } catch (UnknownHostException e) {
178        LOG.error("Error resolving host name: " + e.getMessage(), e);
179        throw e;
180      } catch (IOException e) {
181        LOG.error("Error while trying to perform the initial login: " + e.getMessage(), e);
182        throw e;
183      }
184    }
185    return provider.getCurrent();
186  }
187
188  /**
189   * Checks if security is enabled and if so, launches chore for refreshing kerberos ticket.
190   * @return a ScheduledChore for renewals.
191   */
192  @InterfaceAudience.Private
193  public static ScheduledChore getAuthRenewalChore(final UserGroupInformation user,
194    Configuration conf) {
195    if (!user.hasKerberosCredentials() || !isAuthRenewalChoreEnabled(conf)) {
196      return null;
197    }
198
199    Stoppable stoppable = createDummyStoppable();
200    // if you're in debug mode this is useful to avoid getting spammed by the getTGT()
201    // you can increase this, keeping in mind that the default refresh window is 0.8
202    // e.g. 5min tgt * 0.8 = 4min refresh so interval is better be way less than 1min
203    final int CHECK_TGT_INTERVAL = 30 * 1000; // 30sec
204    return new ScheduledChore("RefreshCredentials", stoppable, CHECK_TGT_INTERVAL) {
205      @Override
206      protected void chore() {
207        try {
208          user.checkTGTAndReloginFromKeytab();
209        } catch (IOException e) {
210          LOG.error("Got exception while trying to refresh credentials: " + e.getMessage(), e);
211        }
212      }
213    };
214  }
215
216  /**
217   * Checks if security is enabled and if so, launches chore for refreshing kerberos ticket.
218   * @param conf the hbase service configuration
219   * @return a ScheduledChore for renewals, if needed, and null otherwise.
220   * @deprecated Deprecated since 2.2.0, this method will be
221   *             {@link org.apache.yetus.audience.InterfaceAudience.Private} use only after 4.0.0.
222   * @see <a href="https://issues.apache.org/jira/browse/HBASE-20886">HBASE-20886</a>
223   */
224  @Deprecated
225  public static ScheduledChore getAuthChore(Configuration conf) throws IOException {
226    if (!isAuthRenewalChoreEnabled(conf)) {
227      return null;
228    }
229    User user = loginClientAsService(conf);
230    return getAuthRenewalChore(user.getUGI(), conf);
231  }
232
233  private static Stoppable createDummyStoppable() {
234    return new Stoppable() {
235      private volatile boolean isStopped = false;
236
237      @Override
238      public void stop(String why) {
239        isStopped = true;
240      }
241
242      @Override
243      public boolean isStopped() {
244        return isStopped;
245      }
246    };
247  }
248
249  /**
250   * Returns whether or not the given name should be interpreted as a group principal. Currently
251   * this simply checks if the name starts with the special group prefix character ("@").
252   */
253  @InterfaceAudience.Private
254  public static boolean isGroupPrincipal(String name) {
255    return name != null && name.startsWith(GROUP_PREFIX);
256  }
257
258  /**
259   * Returns the actual name for a group principal (stripped of the group prefix).
260   */
261  @InterfaceAudience.Private
262  public static String getGroupName(String aclKey) {
263    if (!isGroupPrincipal(aclKey)) {
264      return aclKey;
265    }
266
267    return aclKey.substring(GROUP_PREFIX.length());
268  }
269
270  /**
271   * Returns the group entry with the group prefix for a group principal.
272   */
273  @InterfaceAudience.Private
274  public static String toGroupEntry(String name) {
275    return GROUP_PREFIX + name;
276  }
277
278  /**
279   * Returns true if the chore to automatically renew Kerberos tickets (from keytabs) should be
280   * started. The default is true.
281   */
282  static boolean isAuthRenewalChoreEnabled(Configuration conf) {
283    return conf.getBoolean(HBASE_CLIENT_AUTOMATIC_KEYTAB_RENEWAL_KEY,
284      HBASE_CLIENT_AUTOMATIC_KEYTAB_RENEWAL_DEFAULT);
285  }
286}