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.util.ArrayList;
022import java.util.List;
023import java.util.regex.Pattern;
024import org.apache.commons.lang3.StringUtils;
025import org.apache.hadoop.hbase.HConstants;
026import org.apache.hadoop.hbase.MasterNotRunningException;
027import org.apache.hadoop.hbase.NamespaceDescriptor;
028import org.apache.hadoop.hbase.TableName;
029import org.apache.hadoop.hbase.ZooKeeperConnectionException;
030import org.apache.hadoop.hbase.client.Admin;
031import org.apache.hadoop.hbase.client.Connection;
032import org.apache.hadoop.hbase.client.Table;
033import org.apache.hadoop.hbase.client.TableDescriptor;
034import org.apache.hadoop.hbase.client.security.SecurityCapability;
035import org.apache.hadoop.hbase.ipc.CoprocessorRpcChannel;
036import org.apache.hadoop.hbase.protobuf.generated.AccessControlProtos;
037import org.apache.hadoop.hbase.protobuf.generated.AccessControlProtos.AccessControlService.BlockingInterface;
038import org.apache.hadoop.hbase.util.Bytes;
039import org.apache.yetus.audience.InterfaceAudience;
040
041/**
042 * Utility client for doing access control admin operations.
043 */
044@InterfaceAudience.Public
045public class AccessControlClient {
046  public static final TableName ACL_TABLE_NAME =
047      TableName.valueOf(NamespaceDescriptor.SYSTEM_NAMESPACE_NAME_STR, "acl");
048
049  /**
050   * Return true if authorization is supported and enabled
051   * @param connection The connection to use
052   * @return true if authorization is supported and enabled, false otherwise
053   * @throws IOException
054   */
055  public static boolean isAuthorizationEnabled(Connection connection) throws IOException {
056    return connection.getAdmin().getSecurityCapabilities()
057        .contains(SecurityCapability.AUTHORIZATION);
058  }
059
060  /**
061   * Return true if cell authorization is supported and enabled
062   * @param connection The connection to use
063   * @return true if cell authorization is supported and enabled, false otherwise
064   * @throws IOException
065   */
066  public static boolean isCellAuthorizationEnabled(Connection connection) throws IOException {
067    return connection.getAdmin().getSecurityCapabilities()
068        .contains(SecurityCapability.CELL_AUTHORIZATION);
069  }
070
071  private static BlockingInterface getAccessControlServiceStub(Table ht)
072      throws IOException {
073    CoprocessorRpcChannel service = ht.coprocessorService(HConstants.EMPTY_START_ROW);
074    BlockingInterface protocol =
075        AccessControlProtos.AccessControlService.newBlockingStub(service);
076    return protocol;
077  }
078
079  /**
080   * Grants permission on the specified table for the specified user
081   * @param connection The Connection instance to use
082   * @param tableName
083   * @param userName
084   * @param family
085   * @param qual
086   * @param mergeExistingPermissions If set to false, later granted permissions will override
087   *          previous granted permissions. otherwise, it'll merge with previous granted
088   *          permissions.
089   * @param actions
090   * @throws Throwable
091   */
092  private static void grant(Connection connection, final TableName tableName,
093      final String userName, final byte[] family, final byte[] qual, boolean mergeExistingPermissions,
094      final Permission.Action... actions) throws Throwable {
095    connection.getAdmin().grant(new UserPermission(userName, Permission.newBuilder(tableName)
096        .withFamily(family).withQualifier(qual).withActions(actions).build()),
097      mergeExistingPermissions);
098  }
099
100  /**
101   * Grants permission on the specified table for the specified user.
102   * If permissions for a specified user exists, later granted permissions will override previous granted permissions.
103   * @param connection The Connection instance to use
104   * @param tableName
105   * @param userName
106   * @param family
107   * @param qual
108   * @param actions
109   * @throws Throwable
110   */
111  public static void grant(Connection connection, final TableName tableName, final String userName,
112      final byte[] family, final byte[] qual, final Permission.Action... actions) throws Throwable {
113    grant(connection, tableName, userName, family, qual, true, actions);
114  }
115
116  /**
117   * Grants permission on the specified namespace for the specified user.
118   * @param connection
119   * @param namespace
120   * @param userName
121   * @param mergeExistingPermissions If set to false, later granted permissions will override
122   *          previous granted permissions. otherwise, it'll merge with previous granted
123   *          permissions.
124   * @param actions
125   * @throws Throwable
126   */
127  private static void grant(Connection connection, final String namespace, final String userName,
128      boolean mergeExistingPermissions, final Permission.Action... actions) throws Throwable {
129    connection.getAdmin().grant(
130      new UserPermission(userName, Permission.newBuilder(namespace).withActions(actions).build()),
131      mergeExistingPermissions);
132  }
133
134  /**
135   * Grants permission on the specified namespace for the specified user.
136   * If permissions on the specified namespace exists, later granted permissions will override previous granted
137   * permissions.
138   * @param connection The Connection instance to use
139   * @param namespace
140   * @param userName
141   * @param actions
142   * @throws Throwable
143   */
144  public static void grant(Connection connection, final String namespace, final String userName,
145      final Permission.Action... actions) throws Throwable {
146    grant(connection, namespace, userName, true, actions);
147  }
148
149  /**
150   * Grant global permissions for the specified user.
151   * @param connection
152   * @param userName
153   * @param mergeExistingPermissions If set to false, later granted permissions will override
154   *          previous granted permissions. otherwise, it'll merge with previous granted
155   *          permissions.
156   * @param actions
157   * @throws Throwable
158   */
159  private static void grant(Connection connection, final String userName,
160      boolean mergeExistingPermissions, final Permission.Action... actions) throws Throwable {
161    connection.getAdmin().grant(
162      new UserPermission(userName, Permission.newBuilder().withActions(actions).build()),
163      mergeExistingPermissions);
164  }
165
166  /**
167   * Grant global permissions for the specified user.
168   * If permissions for the specified user exists, later granted permissions will override previous granted
169   * permissions.
170   * @param connection
171   * @param userName
172   * @param actions
173   * @throws Throwable
174   */
175  public static void grant(Connection connection, final String userName,
176      final Permission.Action... actions) throws Throwable {
177    grant(connection, userName, true, actions);
178  }
179
180  public static boolean isAccessControllerRunning(Connection connection)
181      throws MasterNotRunningException, ZooKeeperConnectionException, IOException {
182    try (Admin admin = connection.getAdmin()) {
183      return admin.isTableAvailable(ACL_TABLE_NAME);
184    }
185  }
186
187  /**
188   * Revokes the permission on the table
189   * @param connection The Connection instance to use
190   * @param tableName
191   * @param username
192   * @param family
193   * @param qualifier
194   * @param actions
195   * @throws Throwable
196   */
197  public static void revoke(Connection connection, final TableName tableName,
198      final String username, final byte[] family, final byte[] qualifier,
199      final Permission.Action... actions) throws Throwable {
200    connection.getAdmin().revoke(new UserPermission(username, Permission.newBuilder(tableName)
201        .withFamily(family).withQualifier(qualifier).withActions(actions).build()));
202  }
203
204  /**
205   * Revokes the permission on the namespace for the specified user.
206   * @param connection The Connection instance to use
207   * @param namespace
208   * @param userName
209   * @param actions
210   * @throws Throwable
211   */
212  public static void revoke(Connection connection, final String namespace,
213      final String userName, final Permission.Action... actions) throws Throwable {
214    connection.getAdmin().revoke(
215      new UserPermission(userName, Permission.newBuilder(namespace).withActions(actions).build()));
216  }
217
218  /**
219   * Revoke global permissions for the specified user.
220   * @param connection The Connection instance to use
221   */
222  public static void revoke(Connection connection, final String userName,
223      final Permission.Action... actions) throws Throwable {
224    connection.getAdmin()
225        .revoke(new UserPermission(userName, Permission.newBuilder().withActions(actions).build()));
226  }
227
228  /**
229   * List all the userPermissions matching the given pattern. If pattern is null, the behavior is
230   * dependent on whether user has global admin privileges or not. If yes, the global permissions
231   * along with the list of superusers would be returned. Else, no rows get returned.
232   * @param connection The Connection instance to use
233   * @param tableRegex The regular expression string to match against
234   * @return List of UserPermissions
235   * @throws Throwable
236   */
237  public static List<UserPermission> getUserPermissions(Connection connection, String tableRegex)
238      throws Throwable {
239    return getUserPermissions(connection, tableRegex, HConstants.EMPTY_STRING);
240  }
241
242  /**
243   * List all the userPermissions matching the given table pattern and user name.
244   * @param connection Connection
245   * @param tableRegex The regular expression string to match against
246   * @param userName User name, if empty then all user permissions will be retrieved.
247   * @return List of UserPermissions
248   * @throws Throwable on failure
249   */
250  public static List<UserPermission> getUserPermissions(Connection connection, String tableRegex,
251      String userName) throws Throwable {
252    List<UserPermission> permList = new ArrayList<>();
253    try (Admin admin = connection.getAdmin()) {
254      if (tableRegex == null || tableRegex.isEmpty()) {
255        permList = admin.getUserPermissions(
256          GetUserPermissionsRequest.newBuilder().withUserName(userName).build());
257      } else if (tableRegex.charAt(0) == '@') { // Namespaces
258        String namespaceRegex = tableRegex.substring(1);
259        for (NamespaceDescriptor nsds : admin.listNamespaceDescriptors()) { // Read out all
260                                                                            // namespaces
261          String namespace = nsds.getName();
262          if (namespace.matches(namespaceRegex)) { // Match the given namespace regex?
263            permList.addAll(admin.getUserPermissions(
264              GetUserPermissionsRequest.newBuilder(namespace).withUserName(userName).build()));
265          }
266        }
267      } else { // Tables
268        List<TableDescriptor> htds = admin.listTableDescriptors(Pattern.compile(tableRegex), true);
269        for (TableDescriptor htd : htds) {
270          permList.addAll(admin.getUserPermissions(GetUserPermissionsRequest
271              .newBuilder(htd.getTableName()).withUserName(userName).build()));
272        }
273      }
274    }
275    return permList;
276  }
277
278  /**
279   * List all the userPermissions matching the given table pattern and column family.
280   * @param connection Connection
281   * @param tableRegex The regular expression string to match against. It shouldn't be null, empty
282   *          or a namespace regular expression.
283   * @param columnFamily Column family
284   * @return List of UserPermissions
285   * @throws Throwable on failure
286   */
287  public static List<UserPermission> getUserPermissions(Connection connection, String tableRegex,
288      byte[] columnFamily) throws Throwable {
289    return getUserPermissions(connection, tableRegex, columnFamily, null, HConstants.EMPTY_STRING);
290  }
291
292  /**
293   * List all the userPermissions matching the given table pattern, column family and user name.
294   * @param connection Connection
295   * @param tableRegex The regular expression string to match against. It shouldn't be null, empty
296   *          or a namespace regular expression.
297   * @param columnFamily Column family
298   * @param userName User name, if empty then all user permissions will be retrieved.
299   * @return List of UserPermissions
300   * @throws Throwable on failure
301   */
302  public static List<UserPermission> getUserPermissions(Connection connection, String tableRegex,
303      byte[] columnFamily, String userName) throws Throwable {
304    return getUserPermissions(connection, tableRegex, columnFamily, null, userName);
305  }
306
307  /**
308   * List all the userPermissions matching the given table pattern, column family and column
309   * qualifier.
310   * @param connection Connection
311   * @param tableRegex The regular expression string to match against. It shouldn't be null, empty
312   *          or a namespace regular expression.
313   * @param columnFamily Column family
314   * @param columnQualifier Column qualifier
315   * @return List of UserPermissions
316   * @throws Throwable on failure
317   */
318  public static List<UserPermission> getUserPermissions(Connection connection, String tableRegex,
319      byte[] columnFamily, byte[] columnQualifier) throws Throwable {
320    return getUserPermissions(connection, tableRegex, columnFamily, columnQualifier,
321      HConstants.EMPTY_STRING);
322  }
323
324  /**
325   * List all the userPermissions matching the given table pattern, column family and column
326   * qualifier.
327   * @param connection Connection
328   * @param tableRegex The regular expression string to match against. It shouldn't be null, empty
329   *          or a namespace regular expression.
330   * @param columnFamily Column family
331   * @param columnQualifier Column qualifier
332   * @param userName User name, if empty then all user permissions will be retrieved.
333   * @return List of UserPermissions
334   * @throws Throwable on failure
335   */
336  public static List<UserPermission> getUserPermissions(Connection connection, String tableRegex,
337      byte[] columnFamily, byte[] columnQualifier, String userName) throws Throwable {
338    if (tableRegex == null || tableRegex.isEmpty() || tableRegex.charAt(0) == '@') {
339      throw new IllegalArgumentException("Table name can't be null or empty or a namespace.");
340    }
341    List<UserPermission> permList = new ArrayList<UserPermission>();
342    try (Admin admin = connection.getAdmin()) {
343      List<TableDescriptor> htds = admin.listTableDescriptors(Pattern.compile(tableRegex), true);
344      // Retrieve table permissions
345      for (TableDescriptor htd : htds) {
346        permList.addAll(admin.getUserPermissions(
347          GetUserPermissionsRequest.newBuilder(htd.getTableName()).withFamily(columnFamily)
348              .withQualifier(columnQualifier).withUserName(userName).build()));
349      }
350    }
351    return permList;
352  }
353
354  /**
355   * Validates whether specified user has permission to perform actions on the mentioned table,
356   * column family or column qualifier.
357   * @param connection Connection
358   * @param tableName Table name, it shouldn't be null or empty.
359   * @param columnFamily The column family. Optional argument, can be empty. If empty then
360   *          validation will happen at table level.
361   * @param columnQualifier The column qualifier. Optional argument, can be empty. If empty then
362   *          validation will happen at table and column family level. columnQualifier will not be
363   *          considered if columnFamily is passed as null or empty.
364   * @param userName User name, it shouldn't be null or empty.
365   * @param actions Actions
366   * @return true if access allowed to the specified user, otherwise false.
367   * @throws Throwable on failure
368   */
369  public static boolean hasPermission(Connection connection, String tableName, String columnFamily,
370      String columnQualifier, String userName, Permission.Action... actions) throws Throwable {
371    return hasPermission(connection, tableName, Bytes.toBytes(columnFamily),
372      Bytes.toBytes(columnQualifier), userName, actions);
373  }
374
375  /**
376   * Validates whether specified user has permission to perform actions on the mentioned table,
377   * column family or column qualifier.
378   * @param connection Connection
379   * @param tableName Table name, it shouldn't be null or empty.
380   * @param columnFamily The column family. Optional argument, can be empty. If empty then
381   *          validation will happen at table level.
382   * @param columnQualifier The column qualifier. Optional argument, can be empty. If empty then
383   *          validation will happen at table and column family level. columnQualifier will not be
384   *          considered if columnFamily is passed as null or empty.
385   * @param userName User name, it shouldn't be null or empty.
386   * @param actions Actions
387   * @return true if access allowed to the specified user, otherwise false.
388   * @throws Throwable on failure
389   */
390  public static boolean hasPermission(Connection connection, String tableName, byte[] columnFamily,
391      byte[] columnQualifier, String userName, Permission.Action... actions) throws Throwable {
392    if (StringUtils.isEmpty(tableName) || StringUtils.isEmpty(userName)) {
393      throw new IllegalArgumentException("Table and user name can't be null or empty.");
394    }
395    List<Permission> permissions = new ArrayList<>(1);
396    permissions.add(Permission.newBuilder(TableName.valueOf(tableName)).withFamily(columnFamily)
397        .withQualifier(columnQualifier).withActions(actions).build());
398    return connection.getAdmin().hasUserPermissions(userName, permissions).get(0);
399  }
400}