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 marked as
076 *  {@link org.apache.yetus.audience.InterfaceAudience.Private} in 4.0.0.
077 * @see <a href="https://issues.apache.org/jira/browse/HBASE-20886">HBASE-20886</a>
078 */
079@Deprecated
080@InterfaceAudience.Public
081public final class AuthUtil {
082  private static final Logger LOG = LoggerFactory.getLogger(AuthUtil.class);
083
084  /** Prefix character to denote group names */
085  private static final String GROUP_PREFIX = "@";
086
087  /** Client keytab file */
088  public static final String HBASE_CLIENT_KEYTAB_FILE = "hbase.client.keytab.file";
089
090  /** Client principal */
091  public static final String HBASE_CLIENT_KERBEROS_PRINCIPAL = "hbase.client.keytab.principal";
092
093  private AuthUtil() {
094    super();
095  }
096
097  /**
098   * For kerberized cluster, return login user (from kinit or from keytab if specified).
099   * For non-kerberized cluster, return system user.
100   * @param conf configuartion file
101   * @return user
102   * @throws IOException login exception
103   */
104  @InterfaceAudience.Private
105  public static User loginClient(Configuration conf) throws IOException {
106    UserProvider provider = UserProvider.instantiate(conf);
107    User user = provider.getCurrent();
108    boolean securityOn = provider.isHBaseSecurityEnabled() && provider.isHadoopSecurityEnabled();
109
110    if (securityOn) {
111      boolean fromKeytab = provider.shouldLoginFromKeytab();
112      if (user.getUGI().hasKerberosCredentials()) {
113        // There's already a login user.
114        // But we should avoid misuse credentials which is a dangerous security issue,
115        // so here check whether user specified a keytab and a principal:
116        // 1. Yes, check if user principal match.
117        //    a. match, just return.
118        //    b. mismatch, login using keytab.
119        // 2. No, user may login through kinit, this is the old way, also just return.
120        if (fromKeytab) {
121          return checkPrincipalMatch(conf, user.getUGI().getUserName()) ? user :
122            loginFromKeytabAndReturnUser(provider);
123        }
124        return user;
125      } else if (fromKeytab) {
126        // Kerberos is on and client specify a keytab and principal, but client doesn't login yet.
127        return loginFromKeytabAndReturnUser(provider);
128      }
129    }
130    return user;
131  }
132
133  private static boolean checkPrincipalMatch(Configuration conf, String loginUserName) {
134    String configuredUserName = conf.get(HBASE_CLIENT_KERBEROS_PRINCIPAL);
135    boolean match = configuredUserName.equals(loginUserName);
136    if (!match) {
137      LOG.warn("Trying to login with a different user: {}, existed user is {}.",
138        configuredUserName, loginUserName);
139    }
140    return match;
141  }
142
143  private static User loginFromKeytabAndReturnUser(UserProvider provider) throws IOException {
144    try {
145      provider.login(HBASE_CLIENT_KEYTAB_FILE, HBASE_CLIENT_KERBEROS_PRINCIPAL);
146    } catch (IOException ioe) {
147      LOG.error("Error while trying to login as user {} through {}, with message: {}.",
148        HBASE_CLIENT_KERBEROS_PRINCIPAL, HBASE_CLIENT_KEYTAB_FILE,
149        ioe.getMessage());
150      throw ioe;
151    }
152    return provider.getCurrent();
153  }
154
155  /**
156   * For kerberized cluster, return login user (from kinit or from keytab).
157   * Principal should be the following format: name/fully.qualified.domain.name@REALM.
158   * For non-kerberized cluster, return system user.
159   * <p>
160   * NOT recommend to use to method unless you're sure what you're doing, it is for canary only.
161   * Please use User#loginClient.
162   * @param conf configuration file
163   * @return user
164   * @throws IOException login exception
165   */
166  private static User loginClientAsService(Configuration conf) throws IOException {
167    UserProvider provider = UserProvider.instantiate(conf);
168    if (provider.isHBaseSecurityEnabled() && provider.isHadoopSecurityEnabled()) {
169      try {
170        if (provider.shouldLoginFromKeytab()) {
171          String host = Strings.domainNamePointerToHostName(DNS.getDefaultHost(
172            conf.get("hbase.client.dns.interface", "default"),
173            conf.get("hbase.client.dns.nameserver", "default")));
174          provider.login(HBASE_CLIENT_KEYTAB_FILE, HBASE_CLIENT_KERBEROS_PRINCIPAL, host);
175        }
176      } catch (UnknownHostException e) {
177        LOG.error("Error resolving host name: " + e.getMessage(), e);
178        throw e;
179      } catch (IOException e) {
180        LOG.error("Error while trying to perform the initial login: " + e.getMessage(), e);
181        throw e;
182      }
183    }
184    return provider.getCurrent();
185  }
186
187  /**
188   * Checks if security is enabled and if so, launches chore for refreshing kerberos ticket.
189   * @return a ScheduledChore for renewals.
190   */
191  @InterfaceAudience.Private
192  public static ScheduledChore getAuthRenewalChore(final UserGroupInformation user) {
193    if (!user.hasKerberosCredentials()) {
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    User user = loginClientAsService(conf);
225    return getAuthRenewalChore(user.getUGI());
226  }
227
228  private static Stoppable createDummyStoppable() {
229    return new Stoppable() {
230      private volatile boolean isStopped = false;
231
232      @Override
233      public void stop(String why) {
234        isStopped = true;
235      }
236
237      @Override
238      public boolean isStopped() {
239        return isStopped;
240      }
241    };
242  }
243
244  /**
245   * Returns whether or not the given name should be interpreted as a group
246   * principal.  Currently this simply checks if the name starts with the
247   * special group prefix character ("@").
248   */
249  @InterfaceAudience.Private
250  public static boolean isGroupPrincipal(String name) {
251    return name != null && name.startsWith(GROUP_PREFIX);
252  }
253
254  /**
255   * Returns the actual name for a group principal (stripped of the
256   * group prefix).
257   */
258  @InterfaceAudience.Private
259  public static String getGroupName(String aclKey) {
260    if (!isGroupPrincipal(aclKey)) {
261      return aclKey;
262    }
263
264    return aclKey.substring(GROUP_PREFIX.length());
265  }
266
267  /**
268   * Returns the group entry with the group prefix for a group principal.
269   */
270  @InterfaceAudience.Private
271  public static String toGroupEntry(String name) {
272    return GROUP_PREFIX + name;
273  }
274}