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 n * @throws IOException login exception
104   */
105  @InterfaceAudience.Private
106  public static User loginClient(Configuration conf) throws IOException {
107    UserProvider provider = UserProvider.instantiate(conf);
108    User user = provider.getCurrent();
109    boolean securityOn = provider.isHBaseSecurityEnabled() && provider.isHadoopSecurityEnabled();
110
111    if (securityOn) {
112      boolean fromKeytab = provider.shouldLoginFromKeytab();
113      if (user.getUGI().hasKerberosCredentials()) {
114        // There's already a login user.
115        // But we should avoid misuse credentials which is a dangerous security issue,
116        // so here check whether user specified a keytab and a principal:
117        // 1. Yes, check if user principal match.
118        // a. match, just return.
119        // b. mismatch, login using keytab.
120        // 2. No, user may login through kinit, this is the old way, also just return.
121        if (fromKeytab) {
122          return checkPrincipalMatch(conf, user.getUGI().getUserName())
123            ? user
124            : loginFromKeytabAndReturnUser(provider);
125        }
126        return user;
127      } else if (fromKeytab) {
128        // Kerberos is on and client specify a keytab and principal, but client doesn't login yet.
129        return loginFromKeytabAndReturnUser(provider);
130      }
131    }
132    return user;
133  }
134
135  private static boolean checkPrincipalMatch(Configuration conf, String loginUserName) {
136    String configuredUserName = conf.get(HBASE_CLIENT_KERBEROS_PRINCIPAL);
137    boolean match = configuredUserName.equals(loginUserName);
138    if (!match) {
139      LOG.warn("Trying to login with a different user: {}, existed user is {}.", configuredUserName,
140        loginUserName);
141    }
142    return match;
143  }
144
145  private static User loginFromKeytabAndReturnUser(UserProvider provider) throws IOException {
146    try {
147      provider.login(HBASE_CLIENT_KEYTAB_FILE, HBASE_CLIENT_KERBEROS_PRINCIPAL);
148    } catch (IOException ioe) {
149      LOG.error("Error while trying to login as user {} through {}, with message: {}.",
150        HBASE_CLIENT_KERBEROS_PRINCIPAL, HBASE_CLIENT_KEYTAB_FILE, ioe.getMessage());
151      throw ioe;
152    }
153    return provider.getCurrent();
154  }
155
156  /**
157   * For kerberized cluster, return login user (from kinit or from keytab). Principal should be the
158   * following format: name/fully.qualified.domain.name@REALM. For non-kerberized cluster, return
159   * system user.
160   * <p>
161   * NOT recommend to use to method unless you're sure what you're doing, it is for canary only.
162   * Please use User#loginClient.
163   * @param conf configuration file n * @throws IOException login exception
164   */
165  private static User loginClientAsService(Configuration conf) throws IOException {
166    UserProvider provider = UserProvider.instantiate(conf);
167    if (provider.isHBaseSecurityEnabled() && provider.isHadoopSecurityEnabled()) {
168      try {
169        if (provider.shouldLoginFromKeytab()) {
170          String host = Strings.domainNamePointerToHostName(
171            DNS.getDefaultHost(conf.get("hbase.client.dns.interface", "default"),
172              conf.get("hbase.client.dns.nameserver", "default")));
173          provider.login(HBASE_CLIENT_KEYTAB_FILE, HBASE_CLIENT_KERBEROS_PRINCIPAL, host);
174        }
175      } catch (UnknownHostException e) {
176        LOG.error("Error resolving host name: " + e.getMessage(), e);
177        throw e;
178      } catch (IOException e) {
179        LOG.error("Error while trying to perform the initial login: " + e.getMessage(), e);
180        throw e;
181      }
182    }
183    return provider.getCurrent();
184  }
185
186  /**
187   * Checks if security is enabled and if so, launches chore for refreshing kerberos ticket.
188   * @return a ScheduledChore for renewals.
189   */
190  @InterfaceAudience.Private
191  public static ScheduledChore getAuthRenewalChore(final UserGroupInformation user,
192    Configuration conf) {
193    if (!user.hasKerberosCredentials() || !isAuthRenewalChoreEnabled(conf)) {
194      return null;
195    }
196
197    Stoppable stoppable = createDummyStoppable();
198    // if you're in debug mode this is useful to avoid getting spammed by the getTGT()
199    // you can increase this, keeping in mind that the default refresh window is 0.8
200    // e.g. 5min tgt * 0.8 = 4min refresh so interval is better be way less than 1min
201    final int CHECK_TGT_INTERVAL = 30 * 1000; // 30sec
202    return new ScheduledChore("RefreshCredentials", stoppable, CHECK_TGT_INTERVAL) {
203      @Override
204      protected void chore() {
205        try {
206          user.checkTGTAndReloginFromKeytab();
207        } catch (IOException e) {
208          LOG.error("Got exception while trying to refresh credentials: " + e.getMessage(), e);
209        }
210      }
211    };
212  }
213
214  /**
215   * Checks if security is enabled and if so, launches chore for refreshing kerberos ticket.
216   * @param conf the hbase service configuration
217   * @return a ScheduledChore for renewals, if needed, and null otherwise.
218   * @deprecated Deprecated since 2.2.0, this method will be
219   *             {@link org.apache.yetus.audience.InterfaceAudience.Private} use only after 4.0.0.
220   * @see <a href="https://issues.apache.org/jira/browse/HBASE-20886">HBASE-20886</a>
221   */
222  @Deprecated
223  public static ScheduledChore getAuthChore(Configuration conf) throws IOException {
224    if (!isAuthRenewalChoreEnabled(conf)) {
225      return null;
226    }
227    User user = loginClientAsService(conf);
228    return getAuthRenewalChore(user.getUGI(), conf);
229  }
230
231  private static Stoppable createDummyStoppable() {
232    return new Stoppable() {
233      private volatile boolean isStopped = false;
234
235      @Override
236      public void stop(String why) {
237        isStopped = true;
238      }
239
240      @Override
241      public boolean isStopped() {
242        return isStopped;
243      }
244    };
245  }
246
247  /**
248   * Returns whether or not the given name should be interpreted as a group principal. Currently
249   * this simply checks if the name starts with the special group prefix character ("@").
250   */
251  @InterfaceAudience.Private
252  public static boolean isGroupPrincipal(String name) {
253    return name != null && name.startsWith(GROUP_PREFIX);
254  }
255
256  /**
257   * Returns the actual name for a group principal (stripped of the group prefix).
258   */
259  @InterfaceAudience.Private
260  public static String getGroupName(String aclKey) {
261    if (!isGroupPrincipal(aclKey)) {
262      return aclKey;
263    }
264
265    return aclKey.substring(GROUP_PREFIX.length());
266  }
267
268  /**
269   * Returns the group entry with the group prefix for a group principal.
270   */
271  @InterfaceAudience.Private
272  public static String toGroupEntry(String name) {
273    return GROUP_PREFIX + name;
274  }
275
276  /**
277   * Returns true if the chore to automatically renew Kerberos tickets (from keytabs) should be
278   * started. The default is true.
279   */
280  static boolean isAuthRenewalChoreEnabled(Configuration conf) {
281    return conf.getBoolean(HBASE_CLIENT_AUTOMATIC_KEYTAB_RENEWAL_KEY,
282      HBASE_CLIENT_AUTOMATIC_KEYTAB_RENEWAL_DEFAULT);
283  }
284}