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.security.access;
019
020import java.io.IOException;
021import java.net.InetAddress;
022import java.security.PrivilegedAction;
023import java.security.PrivilegedExceptionAction;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029import java.util.TreeMap;
030import org.apache.hadoop.conf.Configuration;
031import org.apache.hadoop.hbase.AuthUtil;
032import org.apache.hadoop.hbase.Cell;
033import org.apache.hadoop.hbase.CellUtil;
034import org.apache.hadoop.hbase.DoNotRetryIOException;
035import org.apache.hadoop.hbase.HBaseInterfaceAudience;
036import org.apache.hadoop.hbase.NamespaceDescriptor;
037import org.apache.hadoop.hbase.TableName;
038import org.apache.hadoop.hbase.client.RegionInfo;
039import org.apache.hadoop.hbase.ipc.RpcServer;
040import org.apache.hadoop.hbase.security.AccessDeniedException;
041import org.apache.hadoop.hbase.security.Superusers;
042import org.apache.hadoop.hbase.security.User;
043import org.apache.hadoop.hbase.security.UserProvider;
044import org.apache.hadoop.hbase.security.access.Permission.Action;
045import org.apache.hadoop.hbase.util.Bytes;
046import org.apache.hadoop.security.Groups;
047import org.apache.hadoop.security.HadoopKerberosName;
048import org.apache.hadoop.security.UserGroupInformation;
049import org.apache.yetus.audience.InterfaceAudience;
050import org.apache.yetus.audience.InterfaceStability;
051import org.slf4j.Logger;
052import org.slf4j.LoggerFactory;
053
054import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableSet;
055
056@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.COPROC)
057@InterfaceStability.Evolving
058public class AccessChecker {
059  private static final Logger LOG = LoggerFactory.getLogger(AccessChecker.class);
060  private static final Logger AUDITLOG =
061    LoggerFactory.getLogger("SecurityLogger." + AccessChecker.class.getName());
062  private final AuthManager authManager;
063
064  /** Group service to retrieve the user group information */
065  private static Groups groupService;
066
067  public static boolean isAuthorizationSupported(Configuration conf) {
068    return conf.getBoolean(User.HBASE_SECURITY_AUTHORIZATION_CONF_KEY, false);
069  }
070
071  /**
072   * Constructor with existing configuration
073   * @param conf Existing configuration to use
074   */
075  public AccessChecker(final Configuration conf) {
076    this.authManager = new AuthManager(conf);
077    initGroupService(conf);
078  }
079
080  public AuthManager getAuthManager() {
081    return authManager;
082  }
083
084  /**
085   * Authorizes that the current user has any of the given permissions to access the table.
086   * @param user        Active user to which authorization checks should be applied
087   * @param request     Request type.
088   * @param tableName   Table requested
089   * @param permissions Actions being requested
090   * @throws IOException           if obtaining the current user fails
091   * @throws AccessDeniedException if user has no authorization
092   */
093  public void requireAccess(User user, String request, TableName tableName, Action... permissions)
094    throws IOException {
095    AuthResult result = null;
096
097    for (Action permission : permissions) {
098      if (authManager.accessUserTable(user, tableName, permission)) {
099        result = AuthResult.allow(request, "Table permission granted", user, permission, tableName,
100          null, null);
101        break;
102      } else {
103        // rest of the world
104        result = AuthResult.deny(request, "Insufficient permissions", user, permission, tableName,
105          null, null);
106      }
107    }
108    logResult(result);
109    if (!result.isAllowed()) {
110      throw new AccessDeniedException("Insufficient permissions " + result.toContextString());
111    }
112  }
113
114  /**
115   * Authorizes that the current user has global privileges for the given action.
116   * @param user       Active user to which authorization checks should be applied
117   * @param request    Request type
118   * @param filterUser User name to be filtered from permission as requested
119   * @param perm       The action being requested
120   * @throws IOException           if obtaining the current user fails
121   * @throws AccessDeniedException if authorization is denied
122   */
123  public void requirePermission(User user, String request, String filterUser, Action perm)
124    throws IOException {
125    requireGlobalPermission(user, request, perm, null, null, filterUser);
126  }
127
128  /**
129   * Checks that the user has the given global permission. The generated audit log message will
130   * contain context information for the operation being authorized, based on the given parameters.
131   * @param user       Active user to which authorization checks should be applied
132   * @param request    Request type
133   * @param perm       Action being requested
134   * @param tableName  Affected table name.
135   * @param familyMap  Affected column families.
136   * @param filterUser User name to be filtered from permission as requested
137   */
138  public void requireGlobalPermission(User user, String request, Action perm, TableName tableName,
139    Map<byte[], ? extends Collection<byte[]>> familyMap, String filterUser) throws IOException {
140    AuthResult result;
141    if (authManager.authorizeUserGlobal(user, perm)) {
142      result = AuthResult.allow(request, "Global check allowed", user, perm, tableName, familyMap);
143    } else {
144      result = AuthResult.deny(request, "Global check failed", user, perm, tableName, familyMap);
145    }
146    result.getParams().setTableName(tableName).setFamilies(familyMap);
147    result.getParams().addExtraParam("filterUser", filterUser);
148    logResult(result);
149    if (!result.isAllowed()) {
150      throw new AccessDeniedException(
151        "Insufficient permissions for user '" + (user != null ? user.getShortName() : "null")
152          + "' (global, action=" + perm.toString() + ")");
153    }
154  }
155
156  /**
157   * Checks that the user has the given global permission. The generated audit log message will
158   * contain context information for the operation being authorized, based on the given parameters.
159   * @param user      Active user to which authorization checks should be applied
160   * @param request   Request type
161   * @param perm      Action being requested
162   * @param namespace The given namespace
163   */
164  public void requireGlobalPermission(User user, String request, Action perm, String namespace)
165    throws IOException {
166    AuthResult authResult;
167    if (authManager.authorizeUserGlobal(user, perm)) {
168      authResult = AuthResult.allow(request, "Global check allowed", user, perm, null);
169      authResult.getParams().setNamespace(namespace);
170      logResult(authResult);
171    } else {
172      authResult = AuthResult.deny(request, "Global check failed", user, perm, null);
173      authResult.getParams().setNamespace(namespace);
174      logResult(authResult);
175      throw new AccessDeniedException(
176        "Insufficient permissions for user '" + (user != null ? user.getShortName() : "null")
177          + "' (global, action=" + perm.toString() + ")");
178    }
179  }
180
181  /**
182   * Checks that the user has the given global or namespace permission.
183   * @param user        Active user to which authorization checks should be applied
184   * @param request     Request type
185   * @param namespace   Name space as requested
186   * @param filterUser  User name to be filtered from permission as requested
187   * @param permissions Actions being requested
188   */
189  public void requireNamespacePermission(User user, String request, String namespace,
190    String filterUser, Action... permissions) throws IOException {
191    AuthResult result = null;
192
193    for (Action permission : permissions) {
194      if (authManager.authorizeUserNamespace(user, namespace, permission)) {
195        result =
196          AuthResult.allow(request, "Namespace permission granted", user, permission, namespace);
197        break;
198      } else {
199        // rest of the world
200        result = AuthResult.deny(request, "Insufficient permissions", user, permission, namespace);
201      }
202    }
203    result.getParams().addExtraParam("filterUser", filterUser);
204    logResult(result);
205    if (!result.isAllowed()) {
206      throw new AccessDeniedException("Insufficient permissions " + result.toContextString());
207    }
208  }
209
210  /**
211   * Checks that the user has the given global or namespace permission.
212   * @param user        Active user to which authorization checks should be applied
213   * @param request     Request type
214   * @param namespace   The given namespace
215   * @param tableName   Table requested
216   * @param familyMap   Column family map requested
217   * @param permissions Actions being requested
218   */
219  public void requireNamespacePermission(User user, String request, String namespace,
220    TableName tableName, Map<byte[], ? extends Collection<byte[]>> familyMap, Action... permissions)
221    throws IOException {
222    AuthResult result = null;
223
224    for (Action permission : permissions) {
225      if (authManager.authorizeUserNamespace(user, namespace, permission)) {
226        result =
227          AuthResult.allow(request, "Namespace permission granted", user, permission, namespace);
228        result.getParams().setTableName(tableName).setFamilies(familyMap);
229        break;
230      } else {
231        // rest of the world
232        result = AuthResult.deny(request, "Insufficient permissions", user, permission, namespace);
233        result.getParams().setTableName(tableName).setFamilies(familyMap);
234      }
235    }
236    logResult(result);
237    if (!result.isAllowed()) {
238      throw new AccessDeniedException("Insufficient permissions " + result.toContextString());
239    }
240  }
241
242  /**
243   * Authorizes that the current user has any of the given permissions for the given table, column
244   * family and column qualifier.
245   * @param user        Active user to which authorization checks should be applied
246   * @param request     Request type
247   * @param tableName   Table requested
248   * @param family      Column family requested
249   * @param qualifier   Column qualifier requested
250   * @param filterUser  User name to be filtered from permission as requested
251   * @param permissions Actions being requested
252   * @throws IOException           if obtaining the current user fails
253   * @throws AccessDeniedException if user has no authorization
254   */
255  public void requirePermission(User user, String request, TableName tableName, byte[] family,
256    byte[] qualifier, String filterUser, Action... permissions) throws IOException {
257    AuthResult result = null;
258
259    for (Action permission : permissions) {
260      if (authManager.authorizeUserTable(user, tableName, family, qualifier, permission)) {
261        result = AuthResult.allow(request, "Table permission granted", user, permission, tableName,
262          family, qualifier);
263        break;
264      } else {
265        // rest of the world
266        result = AuthResult.deny(request, "Insufficient permissions", user, permission, tableName,
267          family, qualifier);
268      }
269    }
270    result.getParams().addExtraParam("filterUser", filterUser);
271    logResult(result);
272    if (!result.isAllowed()) {
273      throw new AccessDeniedException("Insufficient permissions " + result.toContextString());
274    }
275  }
276
277  /**
278   * Authorizes that the current user has any of the given permissions for the given table, column
279   * family and column qualifier.
280   * @param user      Active user to which authorization checks should be applied
281   * @param request   Request type
282   * @param tableName Table requested
283   * @param family    Column family param
284   * @param qualifier Column qualifier param
285   * @throws IOException           if obtaining the current user fails
286   * @throws AccessDeniedException if user has no authorization
287   */
288  public void requireTablePermission(User user, String request, TableName tableName, byte[] family,
289    byte[] qualifier, Action... permissions) throws IOException {
290    AuthResult result = null;
291
292    for (Action permission : permissions) {
293      if (authManager.authorizeUserTable(user, tableName, permission)) {
294        result = AuthResult.allow(request, "Table permission granted", user, permission, tableName,
295          null, null);
296        result.getParams().setFamily(family).setQualifier(qualifier);
297        break;
298      } else {
299        // rest of the world
300        result = AuthResult.deny(request, "Insufficient permissions", user, permission, tableName,
301          family, qualifier);
302        result.getParams().setFamily(family).setQualifier(qualifier);
303      }
304    }
305    logResult(result);
306    if (!result.isAllowed()) {
307      throw new AccessDeniedException("Insufficient permissions " + result.toContextString());
308    }
309  }
310
311  /**
312   * Check if caller is granting or revoking superusers's or supergroups's permissions.
313   * @param request         request name
314   * @param caller          caller
315   * @param userToBeChecked target user or group
316   * @throws IOException AccessDeniedException if target user is superuser
317   */
318  public void performOnSuperuser(String request, User caller, String userToBeChecked)
319    throws IOException {
320    List<String> userGroups = new ArrayList<>();
321    userGroups.add(userToBeChecked);
322    if (!AuthUtil.isGroupPrincipal(userToBeChecked)) {
323      for (String group : getUserGroups(userToBeChecked)) {
324        userGroups.add(AuthUtil.toGroupEntry(group));
325      }
326    }
327    for (String name : userGroups) {
328      if (Superusers.isSuperUser(name)) {
329        AuthResult result = AuthResult.deny(request,
330          "Granting or revoking superusers's or supergroups's permissions is not allowed", caller,
331          Action.ADMIN, NamespaceDescriptor.SYSTEM_NAMESPACE_NAME_STR);
332        logResult(result);
333        throw new AccessDeniedException(result.getReason());
334      }
335    }
336  }
337
338  public void checkLockPermissions(User user, String namespace, TableName tableName,
339    RegionInfo[] regionInfos, String reason) throws IOException {
340    if (namespace != null && !namespace.isEmpty()) {
341      requireNamespacePermission(user, reason, namespace, null, Action.ADMIN, Action.CREATE);
342    } else if (tableName != null || (regionInfos != null && regionInfos.length > 0)) {
343      // So, either a table or regions op. If latter, check perms ons table.
344      TableName tn = tableName != null ? tableName : regionInfos[0].getTable();
345      requireTablePermission(user, reason, tn, null, null, Action.ADMIN, Action.CREATE);
346    } else {
347      throw new DoNotRetryIOException("Invalid lock level when requesting permissions.");
348    }
349  }
350
351  public static void logResult(AuthResult result) {
352    if (AUDITLOG.isTraceEnabled()) {
353      User user = result.getUser();
354      UserGroupInformation ugi = user != null ? user.getUGI() : null;
355      AUDITLOG.trace(
356        "Access {} for user {}; reason: {}; remote address: {}; request: {}; context: {};"
357          + "auth method: {}",
358        (result.isAllowed() ? "allowed" : "denied"),
359        (user != null ? user.getShortName() : "UNKNOWN"), result.getReason(),
360        RpcServer.getRemoteAddress().map(InetAddress::toString).orElse(""), result.getRequest(),
361        result.toContextString(), ugi != null ? ugi.getAuthenticationMethod() : "UNKNOWN");
362    }
363  }
364
365  /*
366   * Validate the hasPermission operation caller with the filter user. Self check doesn't require
367   * any privilege but for others caller must have ADMIN privilege.
368   */
369  public User validateCallerWithFilterUser(User caller, TablePermission tPerm, String inputUserName)
370    throws IOException {
371    User filterUser = null;
372    if (!caller.getShortName().equals(inputUserName)) {
373      // User should have admin privilege if checking permission for other users
374      requirePermission(caller, "hasPermission", tPerm.getTableName(), tPerm.getFamily(),
375        tPerm.getQualifier(), inputUserName, Action.ADMIN);
376      // Initialize user instance for the input user name
377      List<String> groups = getUserGroups(inputUserName);
378      filterUser = new InputUser(inputUserName, groups.toArray(new String[groups.size()]));
379    } else {
380      // User don't need ADMIN privilege for self check.
381      // Setting action as null in AuthResult to display empty action in audit log
382      AuthResult result = AuthResult.allow("hasPermission", "Self user validation allowed", caller,
383        null, tPerm.getTableName(), tPerm.getFamily(), tPerm.getQualifier());
384      logResult(result);
385      filterUser = caller;
386    }
387    return filterUser;
388  }
389
390  /**
391   * A temporary user class to instantiate User instance based on the name and groups.
392   */
393  public static class InputUser extends User {
394    private String name;
395    private String shortName = null;
396    private String[] groups;
397
398    public InputUser(String name, String[] groups) {
399      this.name = name;
400      this.groups = groups;
401    }
402
403    @Override
404    public String getShortName() {
405      if (this.shortName == null) {
406        try {
407          this.shortName = new HadoopKerberosName(this.name).getShortName();
408        } catch (IOException ioe) {
409          throw new IllegalArgumentException(
410            "Illegal principal name " + this.name + ": " + ioe.toString(), ioe);
411        }
412      }
413      return shortName;
414    }
415
416    @Override
417    public String getName() {
418      return this.name;
419    }
420
421    @Override
422    public String[] getGroupNames() {
423      return this.groups;
424    }
425
426    @Override
427    public <T> T runAs(PrivilegedAction<T> action) {
428      throw new UnsupportedOperationException(
429        "Method not supported, this class has limited implementation");
430    }
431
432    @Override
433    public <T> T runAs(PrivilegedExceptionAction<T> action)
434      throws IOException, InterruptedException {
435      throw new UnsupportedOperationException(
436        "Method not supported, this class has limited implementation");
437    }
438
439    @Override
440    public String toString() {
441      return this.name;
442    }
443  }
444
445  /*
446   * Initialize the group service.
447   */
448  private void initGroupService(Configuration conf) {
449    if (groupService == null) {
450      if (conf.getBoolean(User.TestingGroups.TEST_CONF, false)) {
451        UserProvider.setGroups(new User.TestingGroups(UserProvider.getGroups()));
452        groupService = UserProvider.getGroups();
453      } else {
454        groupService = Groups.getUserToGroupsMappingService(conf);
455      }
456    }
457  }
458
459  /**
460   * Retrieve the groups of the given user.
461   * @param user User name
462   */
463  public static List<String> getUserGroups(String user) {
464    try {
465      return groupService.getGroups(user);
466    } catch (IOException e) {
467      LOG.error("Error occurred while retrieving group for " + user, e);
468      return new ArrayList<>();
469    }
470  }
471
472  /**
473   * Authorizes that if the current user has the given permissions.
474   * @param user       Active user to which authorization checks should be applied
475   * @param request    Request type
476   * @param permission Actions being requested
477   * @return True if the user has the specific permission
478   */
479  public boolean hasUserPermission(User user, String request, Permission permission) {
480    if (permission instanceof TablePermission) {
481      TablePermission tPerm = (TablePermission) permission;
482      for (Permission.Action action : permission.getActions()) {
483        AuthResult authResult = permissionGranted(request, user, action, tPerm.getTableName(),
484          tPerm.getFamily(), tPerm.getQualifier());
485        AccessChecker.logResult(authResult);
486        if (!authResult.isAllowed()) {
487          return false;
488        }
489      }
490    } else if (permission instanceof NamespacePermission) {
491      NamespacePermission nsPerm = (NamespacePermission) permission;
492      AuthResult authResult;
493      for (Action action : nsPerm.getActions()) {
494        if (getAuthManager().authorizeUserNamespace(user, nsPerm.getNamespace(), action)) {
495          authResult =
496            AuthResult.allow(request, "Namespace action allowed", user, action, null, null);
497        } else {
498          authResult =
499            AuthResult.deny(request, "Namespace action denied", user, action, null, null);
500        }
501        AccessChecker.logResult(authResult);
502        if (!authResult.isAllowed()) {
503          return false;
504        }
505      }
506    } else {
507      AuthResult authResult;
508      for (Permission.Action action : permission.getActions()) {
509        if (getAuthManager().authorizeUserGlobal(user, action)) {
510          authResult = AuthResult.allow(request, "Global action allowed", user, action, null, null);
511        } else {
512          authResult = AuthResult.deny(request, "Global action denied", user, action, null, null);
513        }
514        AccessChecker.logResult(authResult);
515        if (!authResult.isAllowed()) {
516          return false;
517        }
518      }
519    }
520    return true;
521  }
522
523  private AuthResult permissionGranted(String request, User user, Action permRequest,
524    TableName tableName, byte[] family, byte[] qualifier) {
525    Map<byte[], ? extends Collection<byte[]>> map = makeFamilyMap(family, qualifier);
526    return permissionGranted(request, user, permRequest, tableName, map);
527  }
528
529  /**
530   * Check the current user for authorization to perform a specific action against the given set of
531   * row data.
532   * <p>
533   * Note: Ordering of the authorization checks has been carefully optimized to short-circuit the
534   * most common requests and minimize the amount of processing required.
535   * </p>
536   * @param request     User request
537   * @param user        User name
538   * @param permRequest the action being requested
539   * @param tableName   Table name
540   * @param families    the map of column families to qualifiers present in the request
541   * @return an authorization result
542   */
543  public AuthResult permissionGranted(String request, User user, Action permRequest,
544    TableName tableName, Map<byte[], ? extends Collection<?>> families) {
545    // 1. All users need read access to hbase:meta table.
546    // this is a very common operation, so deal with it quickly.
547    if (TableName.META_TABLE_NAME.equals(tableName)) {
548      if (permRequest == Action.READ) {
549        return AuthResult.allow(request, "All users allowed", user, permRequest, tableName,
550          families);
551      }
552    }
553
554    if (user == null) {
555      return AuthResult.deny(request, "No user associated with request!", null, permRequest,
556        tableName, families);
557    }
558
559    // 2. check for the table-level, if successful we can short-circuit
560    if (getAuthManager().authorizeUserTable(user, tableName, permRequest)) {
561      return AuthResult.allow(request, "Table permission granted", user, permRequest, tableName,
562        families);
563    }
564
565    // 3. check permissions against the requested families
566    if (families != null && families.size() > 0) {
567      // all families must pass
568      for (Map.Entry<byte[], ? extends Collection<?>> family : families.entrySet()) {
569        // a) check for family level access
570        if (getAuthManager().authorizeUserTable(user, tableName, family.getKey(), permRequest)) {
571          continue; // family-level permission overrides per-qualifier
572        }
573
574        // b) qualifier level access can still succeed
575        if ((family.getValue() != null) && (family.getValue().size() > 0)) {
576          if (family.getValue() instanceof Set) {
577            // for each qualifier of the family
578            Set<byte[]> familySet = (Set<byte[]>) family.getValue();
579            for (byte[] qualifier : familySet) {
580              if (
581                !getAuthManager().authorizeUserTable(user, tableName, family.getKey(), qualifier,
582                  permRequest)
583              ) {
584                return AuthResult.deny(request, "Failed qualifier check", user, permRequest,
585                  tableName, makeFamilyMap(family.getKey(), qualifier));
586              }
587            }
588          } else if (family.getValue() instanceof List) { // List<Cell>
589            List<Cell> cellList = (List<Cell>) family.getValue();
590            for (Cell cell : cellList) {
591              if (
592                !getAuthManager().authorizeUserTable(user, tableName, family.getKey(),
593                  CellUtil.cloneQualifier(cell), permRequest)
594              ) {
595                return AuthResult.deny(request, "Failed qualifier check", user, permRequest,
596                  tableName, makeFamilyMap(family.getKey(), CellUtil.cloneQualifier(cell)));
597              }
598            }
599          }
600        } else {
601          // no qualifiers and family-level check already failed
602          return AuthResult.deny(request, "Failed family check", user, permRequest, tableName,
603            makeFamilyMap(family.getKey(), null));
604        }
605      }
606
607      // all family checks passed
608      return AuthResult.allow(request, "All family checks passed", user, permRequest, tableName,
609        families);
610    }
611
612    // 4. no families to check and table level access failed
613    return AuthResult.deny(request, "No families to check and table permission failed", user,
614      permRequest, tableName, families);
615  }
616
617  private Map<byte[], ? extends Collection<byte[]>> makeFamilyMap(byte[] family, byte[] qualifier) {
618    if (family == null) {
619      return null;
620    }
621
622    Map<byte[], Collection<byte[]>> familyMap = new TreeMap<>(Bytes.BYTES_COMPARATOR);
623    familyMap.put(family, qualifier != null ? ImmutableSet.of(qualifier) : null);
624    return familyMap;
625  }
626}