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