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