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