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.visibility;
019
020import static org.apache.hadoop.hbase.HConstants.OperationStatusCode.SANITY_CHECK_FAILURE;
021import static org.apache.hadoop.hbase.HConstants.OperationStatusCode.SUCCESS;
022import static org.apache.hadoop.hbase.security.visibility.VisibilityConstants.LABELS_TABLE_FAMILY;
023import static org.apache.hadoop.hbase.security.visibility.VisibilityConstants.LABELS_TABLE_NAME;
024
025import com.google.protobuf.ByteString;
026import com.google.protobuf.RpcCallback;
027import com.google.protobuf.RpcController;
028import com.google.protobuf.Service;
029import java.io.IOException;
030import java.net.InetAddress;
031import java.util.ArrayList;
032import java.util.Collections;
033import java.util.HashMap;
034import java.util.Iterator;
035import java.util.List;
036import java.util.Map;
037import java.util.Objects;
038import java.util.Optional;
039import org.apache.hadoop.conf.Configuration;
040import org.apache.hadoop.hbase.AuthUtil;
041import org.apache.hadoop.hbase.Cell;
042import org.apache.hadoop.hbase.CellScanner;
043import org.apache.hadoop.hbase.CoprocessorEnvironment;
044import org.apache.hadoop.hbase.DoNotRetryIOException;
045import org.apache.hadoop.hbase.HBaseInterfaceAudience;
046import org.apache.hadoop.hbase.PrivateCellUtil;
047import org.apache.hadoop.hbase.TableName;
048import org.apache.hadoop.hbase.Tag;
049import org.apache.hadoop.hbase.TagType;
050import org.apache.hadoop.hbase.client.Admin;
051import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
052import org.apache.hadoop.hbase.client.Delete;
053import org.apache.hadoop.hbase.client.Get;
054import org.apache.hadoop.hbase.client.MasterSwitchType;
055import org.apache.hadoop.hbase.client.Mutation;
056import org.apache.hadoop.hbase.client.Put;
057import org.apache.hadoop.hbase.client.Result;
058import org.apache.hadoop.hbase.client.Scan;
059import org.apache.hadoop.hbase.client.TableDescriptor;
060import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
061import org.apache.hadoop.hbase.constraint.ConstraintException;
062import org.apache.hadoop.hbase.coprocessor.CoprocessorException;
063import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
064import org.apache.hadoop.hbase.coprocessor.CoreCoprocessor;
065import org.apache.hadoop.hbase.coprocessor.MasterCoprocessor;
066import org.apache.hadoop.hbase.coprocessor.MasterCoprocessorEnvironment;
067import org.apache.hadoop.hbase.coprocessor.MasterObserver;
068import org.apache.hadoop.hbase.coprocessor.ObserverContext;
069import org.apache.hadoop.hbase.coprocessor.RegionCoprocessor;
070import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
071import org.apache.hadoop.hbase.coprocessor.RegionObserver;
072import org.apache.hadoop.hbase.exceptions.DeserializationException;
073import org.apache.hadoop.hbase.filter.Filter;
074import org.apache.hadoop.hbase.filter.FilterBase;
075import org.apache.hadoop.hbase.filter.FilterList;
076import org.apache.hadoop.hbase.io.hfile.HFile;
077import org.apache.hadoop.hbase.ipc.CoprocessorRpcUtils;
078import org.apache.hadoop.hbase.ipc.RpcServer;
079import org.apache.hadoop.hbase.protobuf.generated.ClientProtos.RegionActionResult;
080import org.apache.hadoop.hbase.protobuf.generated.HBaseProtos.NameBytesPair;
081import org.apache.hadoop.hbase.protobuf.generated.VisibilityLabelsProtos;
082import org.apache.hadoop.hbase.protobuf.generated.VisibilityLabelsProtos.GetAuthsRequest;
083import org.apache.hadoop.hbase.protobuf.generated.VisibilityLabelsProtos.GetAuthsResponse;
084import org.apache.hadoop.hbase.protobuf.generated.VisibilityLabelsProtos.ListLabelsRequest;
085import org.apache.hadoop.hbase.protobuf.generated.VisibilityLabelsProtos.ListLabelsResponse;
086import org.apache.hadoop.hbase.protobuf.generated.VisibilityLabelsProtos.SetAuthsRequest;
087import org.apache.hadoop.hbase.protobuf.generated.VisibilityLabelsProtos.VisibilityLabel;
088import org.apache.hadoop.hbase.protobuf.generated.VisibilityLabelsProtos.VisibilityLabelsRequest;
089import org.apache.hadoop.hbase.protobuf.generated.VisibilityLabelsProtos.VisibilityLabelsResponse;
090import org.apache.hadoop.hbase.protobuf.generated.VisibilityLabelsProtos.VisibilityLabelsService;
091import org.apache.hadoop.hbase.regionserver.BloomType;
092import org.apache.hadoop.hbase.regionserver.DisabledRegionSplitPolicy;
093import org.apache.hadoop.hbase.regionserver.InternalScanner;
094import org.apache.hadoop.hbase.regionserver.MiniBatchOperationInProgress;
095import org.apache.hadoop.hbase.regionserver.OperationStatus;
096import org.apache.hadoop.hbase.regionserver.Region;
097import org.apache.hadoop.hbase.regionserver.RegionScanner;
098import org.apache.hadoop.hbase.regionserver.querymatcher.DeleteTracker;
099import org.apache.hadoop.hbase.security.AccessDeniedException;
100import org.apache.hadoop.hbase.security.Superusers;
101import org.apache.hadoop.hbase.security.User;
102import org.apache.hadoop.hbase.security.access.AccessChecker;
103import org.apache.hadoop.hbase.security.access.AccessController;
104import org.apache.hadoop.hbase.util.Bytes;
105import org.apache.hadoop.hbase.util.Pair;
106import org.apache.hadoop.util.StringUtils;
107import org.apache.yetus.audience.InterfaceAudience;
108import org.slf4j.Logger;
109import org.slf4j.LoggerFactory;
110
111import org.apache.hbase.thirdparty.com.google.common.collect.Lists;
112import org.apache.hbase.thirdparty.com.google.common.collect.MapMaker;
113
114/**
115 * Coprocessor that has both the MasterObserver and RegionObserver implemented that supports in
116 * visibility labels
117 */
118@CoreCoprocessor
119@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.CONFIG)
120// TODO: break out Observer functions into separate class/sub-class.
121public class VisibilityController implements MasterCoprocessor, RegionCoprocessor,
122  VisibilityLabelsService.Interface, MasterObserver, RegionObserver {
123
124  private static final Logger LOG = LoggerFactory.getLogger(VisibilityController.class);
125  private static final Logger AUDITLOG =
126    LoggerFactory.getLogger("SecurityLogger." + VisibilityController.class.getName());
127  // flags if we are running on a region of the 'labels' table
128  private boolean labelsRegion = false;
129  // Flag denoting whether AcessController is available or not.
130  private boolean accessControllerAvailable = false;
131  private Configuration conf;
132  private volatile boolean initialized = false;
133  private boolean checkAuths = false;
134  /** Mapping of scanner instances to the user who created them */
135  private Map<InternalScanner, String> scannerOwners = new MapMaker().weakKeys().makeMap();
136
137  private VisibilityLabelService visibilityLabelService;
138
139  /**
140   * if we are active, usually false, only true if "hbase.security.authorization" has been set to
141   * true in site configuration
142   */
143  boolean authorizationEnabled;
144
145  // Add to this list if there are any reserved tag types
146  private static ArrayList<Byte> RESERVED_VIS_TAG_TYPES = new ArrayList<>();
147  static {
148    RESERVED_VIS_TAG_TYPES.add(TagType.VISIBILITY_TAG_TYPE);
149    RESERVED_VIS_TAG_TYPES.add(TagType.VISIBILITY_EXP_SERIALIZATION_FORMAT_TAG_TYPE);
150    RESERVED_VIS_TAG_TYPES.add(TagType.STRING_VIS_TAG_TYPE);
151  }
152
153  public static boolean isCellAuthorizationSupported(Configuration conf) {
154    return AccessChecker.isAuthorizationSupported(conf);
155  }
156
157  @Override
158  public void start(CoprocessorEnvironment env) throws IOException {
159    this.conf = env.getConfiguration();
160
161    authorizationEnabled = AccessChecker.isAuthorizationSupported(conf);
162    if (!authorizationEnabled) {
163      LOG.warn("The VisibilityController has been loaded with authorization checks disabled.");
164    }
165
166    if (HFile.getFormatVersion(conf) < HFile.MIN_FORMAT_VERSION_WITH_TAGS) {
167      throw new RuntimeException("A minimum HFile version of " + HFile.MIN_FORMAT_VERSION_WITH_TAGS
168        + " is required to persist visibility labels. Consider setting " + HFile.FORMAT_VERSION_KEY
169        + " accordingly.");
170    }
171
172    // Do not create for master CPs
173    if (!(env instanceof MasterCoprocessorEnvironment)) {
174      visibilityLabelService =
175        VisibilityLabelServiceManager.getInstance().getVisibilityLabelService(this.conf);
176    }
177  }
178
179  @Override
180  public void stop(CoprocessorEnvironment env) throws IOException {
181
182  }
183
184  /**************************** Observer/Service Getters ************************************/
185  @Override
186  public Optional<RegionObserver> getRegionObserver() {
187    return Optional.of(this);
188  }
189
190  @Override
191  public Optional<MasterObserver> getMasterObserver() {
192    return Optional.of(this);
193  }
194
195  @Override
196  public Iterable<Service> getServices() {
197    return Collections
198      .singleton(VisibilityLabelsProtos.VisibilityLabelsService.newReflectiveService(this));
199  }
200
201  /********************************* Master related hooks **********************************/
202
203  @Override
204  public void postStartMaster(ObserverContext<MasterCoprocessorEnvironment> ctx)
205    throws IOException {
206    // Need to create the new system table for labels here
207    try (Admin admin = ctx.getEnvironment().getConnection().getAdmin()) {
208      if (!admin.tableExists(LABELS_TABLE_NAME)) {
209        // We will cache all the labels. No need of normal table block cache.
210        // Let the "labels" table having only one region always. We are not expecting too many
211        // labels in the system.
212        TableDescriptor tableDescriptor = TableDescriptorBuilder.newBuilder(LABELS_TABLE_NAME)
213          .setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder(LABELS_TABLE_FAMILY)
214            .setBloomFilterType(BloomType.NONE).setBlockCacheEnabled(false).build())
215          .setValue(TableDescriptorBuilder.SPLIT_POLICY, DisabledRegionSplitPolicy.class.getName())
216          .build();
217
218        admin.createTable(tableDescriptor);
219      }
220    }
221  }
222
223  @Override
224  public TableDescriptor preModifyTable(ObserverContext<MasterCoprocessorEnvironment> ctx,
225    TableName tableName, TableDescriptor currentDescriptor, TableDescriptor newDescriptor)
226    throws IOException {
227    if (authorizationEnabled) {
228      if (LABELS_TABLE_NAME.equals(tableName)) {
229        throw new ConstraintException("Cannot alter " + LABELS_TABLE_NAME);
230      }
231    }
232    return newDescriptor;
233  }
234
235  @Override
236  public void preDisableTable(ObserverContext<MasterCoprocessorEnvironment> ctx,
237    TableName tableName) throws IOException {
238    if (!authorizationEnabled) {
239      return;
240    }
241    if (LABELS_TABLE_NAME.equals(tableName)) {
242      throw new ConstraintException("Cannot disable " + LABELS_TABLE_NAME);
243    }
244  }
245
246  /****************************** Region related hooks ******************************/
247
248  @Override
249  public void postOpen(ObserverContext<RegionCoprocessorEnvironment> e) {
250    // Read the entire labels table and populate the zk
251    if (e.getEnvironment().getRegion().getRegionInfo().getTable().equals(LABELS_TABLE_NAME)) {
252      this.labelsRegion = true;
253      synchronized (this) {
254        this.accessControllerAvailable =
255          CoprocessorHost.getLoadedCoprocessors().contains(AccessController.class.getName());
256      }
257      initVisibilityLabelService(e.getEnvironment());
258    } else {
259      checkAuths = e.getEnvironment().getConfiguration()
260        .getBoolean(VisibilityConstants.CHECK_AUTHS_FOR_MUTATION, false);
261      initVisibilityLabelService(e.getEnvironment());
262    }
263  }
264
265  private void initVisibilityLabelService(RegionCoprocessorEnvironment env) {
266    try {
267      this.visibilityLabelService.init(env);
268      this.initialized = true;
269    } catch (IOException ioe) {
270      LOG.error("Error while initializing VisibilityLabelService..", ioe);
271      throw new RuntimeException(ioe);
272    }
273  }
274
275  @Override
276  public void postSetSplitOrMergeEnabled(final ObserverContext<MasterCoprocessorEnvironment> ctx,
277    final boolean newValue, final MasterSwitchType switchType) throws IOException {
278  }
279
280  @Override
281  public void preBatchMutate(ObserverContext<RegionCoprocessorEnvironment> c,
282    MiniBatchOperationInProgress<Mutation> miniBatchOp) throws IOException {
283    if (c.getEnvironment().getRegion().getRegionInfo().getTable().isSystemTable()) {
284      return;
285    }
286    // TODO this can be made as a global LRU cache at HRS level?
287    Map<String, List<Tag>> labelCache = new HashMap<>();
288    for (int i = 0; i < miniBatchOp.size(); i++) {
289      Mutation m = miniBatchOp.getOperation(i);
290      CellVisibility cellVisibility = null;
291      try {
292        cellVisibility = m.getCellVisibility();
293      } catch (DeserializationException de) {
294        miniBatchOp.setOperationStatus(i,
295          new OperationStatus(SANITY_CHECK_FAILURE, de.getMessage()));
296        continue;
297      }
298      boolean sanityFailure = false;
299      boolean modifiedTagFound = false;
300      Pair<Boolean, Tag> pair = new Pair<>(false, null);
301      for (CellScanner cellScanner = m.cellScanner(); cellScanner.advance();) {
302        pair = checkForReservedVisibilityTagPresence(cellScanner.current(), pair);
303        if (!pair.getFirst()) {
304          // Don't disallow reserved tags if authorization is disabled
305          if (authorizationEnabled) {
306            miniBatchOp.setOperationStatus(i, new OperationStatus(SANITY_CHECK_FAILURE,
307              "Mutation contains cell with reserved type tag"));
308            sanityFailure = true;
309          }
310          break;
311        } else {
312          // Indicates that the cell has a the tag which was modified in the src replication cluster
313          Tag tag = pair.getSecond();
314          if (cellVisibility == null && tag != null) {
315            // May need to store only the first one
316            cellVisibility = new CellVisibility(Tag.getValueAsString(tag));
317            modifiedTagFound = true;
318          }
319        }
320      }
321      if (!sanityFailure && (m instanceof Put || m instanceof Delete)) {
322        if (cellVisibility != null) {
323          String labelsExp = cellVisibility.getExpression();
324          List<Tag> visibilityTags = labelCache.get(labelsExp);
325          if (visibilityTags == null) {
326            // Don't check user auths for labels with Mutations when the user is super user
327            boolean authCheck = authorizationEnabled && checkAuths && !(isSystemOrSuperUser());
328            try {
329              visibilityTags =
330                this.visibilityLabelService.createVisibilityExpTags(labelsExp, true, authCheck);
331            } catch (InvalidLabelException e) {
332              miniBatchOp.setOperationStatus(i,
333                new OperationStatus(SANITY_CHECK_FAILURE, e.getMessage()));
334            }
335            if (visibilityTags != null) {
336              labelCache.put(labelsExp, visibilityTags);
337            }
338          }
339          if (visibilityTags != null) {
340            List<Cell> updatedCells = new ArrayList<>();
341            for (CellScanner cellScanner = m.cellScanner(); cellScanner.advance();) {
342              Cell cell = cellScanner.current();
343              List<Tag> tags = PrivateCellUtil.getTags(cell);
344              if (modifiedTagFound) {
345                // Rewrite the tags by removing the modified tags.
346                removeReplicationVisibilityTag(tags);
347              }
348              tags.addAll(visibilityTags);
349              Cell updatedCell = PrivateCellUtil.createCell(cell, tags);
350              updatedCells.add(updatedCell);
351            }
352            m.getFamilyCellMap().clear();
353            // Clear and add new Cells to the Mutation.
354            for (Cell cell : updatedCells) {
355              if (m instanceof Put) {
356                Put p = (Put) m;
357                p.add(cell);
358              } else {
359                Delete d = (Delete) m;
360                d.add(cell);
361              }
362            }
363          }
364        }
365      }
366    }
367  }
368
369  @Override
370  public void prePrepareTimeStampForDeleteVersion(ObserverContext<RegionCoprocessorEnvironment> ctx,
371    Mutation delete, Cell cell, byte[] byteNow, Get get) throws IOException {
372    // Nothing to do if we are not filtering by visibility
373    if (!authorizationEnabled) {
374      return;
375    }
376
377    CellVisibility cellVisibility = null;
378    try {
379      cellVisibility = delete.getCellVisibility();
380    } catch (DeserializationException de) {
381      throw new IOException("Invalid cell visibility specified " + delete, de);
382    }
383    // The check for checkForReservedVisibilityTagPresence happens in preBatchMutate happens.
384    // It happens for every mutation and that would be enough.
385    List<Tag> visibilityTags = new ArrayList<>();
386    if (cellVisibility != null) {
387      String labelsExp = cellVisibility.getExpression();
388      try {
389        visibilityTags =
390          this.visibilityLabelService.createVisibilityExpTags(labelsExp, false, false);
391      } catch (InvalidLabelException e) {
392        throw new IOException("Invalid cell visibility specified " + labelsExp, e);
393      }
394    }
395    get.setFilter(new DeleteVersionVisibilityExpressionFilter(visibilityTags,
396      VisibilityConstants.SORTED_ORDINAL_SERIALIZATION_FORMAT));
397    try (RegionScanner scanner = ctx.getEnvironment().getRegion().getScanner(new Scan(get))) {
398      // NOTE: Please don't use HRegion.get() instead,
399      // because it will copy cells to heap. See HBASE-26036
400      List<Cell> result = new ArrayList<>();
401      scanner.next(result);
402
403      if (result.size() < get.getMaxVersions()) {
404        // Nothing to delete
405        PrivateCellUtil.updateLatestStamp(cell, byteNow);
406        return;
407      }
408      if (result.size() > get.getMaxVersions()) {
409        throw new RuntimeException(
410          "Unexpected size: " + result.size() + ". Results more than the max versions obtained.");
411      }
412      Cell getCell = result.get(get.getMaxVersions() - 1);
413      PrivateCellUtil.setTimestamp(cell, getCell.getTimestamp());
414    }
415    // We are bypassing here because in the HRegion.updateDeleteLatestVersionTimeStamp we would
416    // update with the current timestamp after again doing a get. As the hook as already determined
417    // the needed timestamp we need to bypass here.
418    // TODO : See if HRegion.updateDeleteLatestVersionTimeStamp() could be
419    // called only if the hook is not called.
420    ctx.bypass();
421  }
422
423  /**
424   * Checks whether cell contains any tag with type as VISIBILITY_TAG_TYPE. This tag type is
425   * reserved and should not be explicitly set by user.
426   * @param cell The cell under consideration
427   * @param pair An optional pair of type {@code <Boolean, Tag>} which would be reused if already
428   *             set and new one will be created if NULL is passed
429   * @return If the boolean is false then it indicates that the cell has a RESERVERD_VIS_TAG and
430   *         with boolean as true, not null tag indicates that a string modified tag was found.
431   */
432  private Pair<Boolean, Tag> checkForReservedVisibilityTagPresence(Cell cell,
433    Pair<Boolean, Tag> pair) throws IOException {
434    if (pair == null) {
435      pair = new Pair<>(false, null);
436    } else {
437      pair.setFirst(false);
438      pair.setSecond(null);
439    }
440    // Bypass this check when the operation is done by a system/super user.
441    // This is done because, while Replication, the Cells coming to the peer cluster with reserved
442    // typed tags and this is fine and should get added to the peer cluster table
443    if (isSystemOrSuperUser()) {
444      // Does the cell contain special tag which indicates that the replicated
445      // cell visiblilty tags
446      // have been modified
447      Tag modifiedTag = null;
448      Iterator<Tag> tagsIterator = PrivateCellUtil.tagsIterator(cell);
449      while (tagsIterator.hasNext()) {
450        Tag tag = tagsIterator.next();
451        if (tag.getType() == TagType.STRING_VIS_TAG_TYPE) {
452          modifiedTag = tag;
453          break;
454        }
455      }
456      pair.setFirst(true);
457      pair.setSecond(modifiedTag);
458      return pair;
459    }
460    Iterator<Tag> tagsItr = PrivateCellUtil.tagsIterator(cell);
461    while (tagsItr.hasNext()) {
462      if (RESERVED_VIS_TAG_TYPES.contains(tagsItr.next().getType())) {
463        return pair;
464      }
465    }
466    pair.setFirst(true);
467    return pair;
468  }
469
470  private void removeReplicationVisibilityTag(List<Tag> tags) throws IOException {
471    Iterator<Tag> iterator = tags.iterator();
472    while (iterator.hasNext()) {
473      Tag tag = iterator.next();
474      if (tag.getType() == TagType.STRING_VIS_TAG_TYPE) {
475        iterator.remove();
476        break;
477      }
478    }
479  }
480
481  @Override
482  public void preScannerOpen(ObserverContext<RegionCoprocessorEnvironment> e, Scan scan)
483    throws IOException {
484    if (!initialized) {
485      throw new VisibilityControllerNotReadyException("VisibilityController not yet initialized!");
486    }
487    // Nothing to do if authorization is not enabled
488    if (!authorizationEnabled) {
489      return;
490    }
491    Region region = e.getEnvironment().getRegion();
492    Authorizations authorizations = null;
493    try {
494      authorizations = scan.getAuthorizations();
495    } catch (DeserializationException de) {
496      throw new IOException(de);
497    }
498    if (authorizations == null) {
499      // No Authorizations present for this scan/Get!
500      // In case of system tables other than "labels" just scan with out visibility check and
501      // filtering. Checking visibility labels for META and NAMESPACE table is not needed.
502      TableName table = region.getRegionInfo().getTable();
503      if (table.isSystemTable() && !table.equals(LABELS_TABLE_NAME)) {
504        return;
505      }
506    }
507
508    Filter visibilityLabelFilter =
509      VisibilityUtils.createVisibilityLabelFilter(region, authorizations);
510    if (visibilityLabelFilter != null) {
511      Filter filter = scan.getFilter();
512      if (filter != null) {
513        scan.setFilter(new FilterList(filter, visibilityLabelFilter));
514      } else {
515        scan.setFilter(visibilityLabelFilter);
516      }
517    }
518  }
519
520  @Override
521  public DeleteTracker postInstantiateDeleteTracker(
522    ObserverContext<RegionCoprocessorEnvironment> ctx, DeleteTracker delTracker)
523    throws IOException {
524    // Nothing to do if we are not filtering by visibility
525    if (!authorizationEnabled) {
526      return delTracker;
527    }
528    Region region = ctx.getEnvironment().getRegion();
529    TableName table = region.getRegionInfo().getTable();
530    if (table.isSystemTable()) {
531      return delTracker;
532    }
533    // We are creating a new type of delete tracker here which is able to track
534    // the timestamps and also the
535    // visibility tags per cell. The covering cells are determined not only
536    // based on the delete type and ts
537    // but also on the visibility expression matching.
538    return new VisibilityScanDeleteTracker(delTracker.getCellComparator());
539  }
540
541  @Override
542  public RegionScanner postScannerOpen(final ObserverContext<RegionCoprocessorEnvironment> c,
543    final Scan scan, final RegionScanner s) throws IOException {
544    User user = VisibilityUtils.getActiveUser();
545    if (user != null && user.getShortName() != null) {
546      scannerOwners.put(s, user.getShortName());
547    }
548    return s;
549  }
550
551  @Override
552  public boolean preScannerNext(final ObserverContext<RegionCoprocessorEnvironment> c,
553    final InternalScanner s, final List<Result> result, final int limit, final boolean hasNext)
554    throws IOException {
555    requireScannerOwner(s);
556    return hasNext;
557  }
558
559  @Override
560  public void preScannerClose(final ObserverContext<RegionCoprocessorEnvironment> c,
561    final InternalScanner s) throws IOException {
562    requireScannerOwner(s);
563  }
564
565  @Override
566  public void postScannerClose(final ObserverContext<RegionCoprocessorEnvironment> c,
567    final InternalScanner s) throws IOException {
568    // clean up any associated owner mapping
569    scannerOwners.remove(s);
570  }
571
572  /**
573   * Verify, when servicing an RPC, that the caller is the scanner owner. If so, we assume that
574   * access control is correctly enforced based on the checks performed in preScannerOpen()
575   */
576  private void requireScannerOwner(InternalScanner s) throws AccessDeniedException {
577    if (!RpcServer.isInRpcCallContext()) return;
578    String requestUName = RpcServer.getRequestUserName().orElse(null);
579    String owner = scannerOwners.get(s);
580    if (authorizationEnabled && owner != null && !owner.equals(requestUName)) {
581      throw new AccessDeniedException("User '" + requestUName + "' is not the scanner owner!");
582    }
583  }
584
585  @Override
586  public void preGetOp(ObserverContext<RegionCoprocessorEnvironment> e, Get get, List<Cell> results)
587    throws IOException {
588    if (!initialized) {
589      throw new VisibilityControllerNotReadyException("VisibilityController not yet initialized");
590    }
591    // Nothing useful to do if authorization is not enabled
592    if (!authorizationEnabled) {
593      return;
594    }
595    Region region = e.getEnvironment().getRegion();
596    Authorizations authorizations = null;
597    try {
598      authorizations = get.getAuthorizations();
599    } catch (DeserializationException de) {
600      throw new IOException(de);
601    }
602    if (authorizations == null) {
603      // No Authorizations present for this scan/Get!
604      // In case of system tables other than "labels" just scan with out visibility check and
605      // filtering. Checking visibility labels for META and NAMESPACE table is not needed.
606      TableName table = region.getRegionInfo().getTable();
607      if (table.isSystemTable() && !table.equals(LABELS_TABLE_NAME)) {
608        return;
609      }
610    }
611    Filter visibilityLabelFilter =
612      VisibilityUtils.createVisibilityLabelFilter(e.getEnvironment().getRegion(), authorizations);
613    if (visibilityLabelFilter != null) {
614      Filter filter = get.getFilter();
615      if (filter != null) {
616        get.setFilter(new FilterList(filter, visibilityLabelFilter));
617      } else {
618        get.setFilter(visibilityLabelFilter);
619      }
620    }
621  }
622
623  private boolean isSystemOrSuperUser() throws IOException {
624    return Superusers.isSuperUser(VisibilityUtils.getActiveUser());
625  }
626
627  @Override
628  public List<Pair<Cell, Cell>> postIncrementBeforeWAL(
629    ObserverContext<RegionCoprocessorEnvironment> ctx, Mutation mutation,
630    List<Pair<Cell, Cell>> cellPairs) throws IOException {
631    List<Pair<Cell, Cell>> resultPairs = new ArrayList<>(cellPairs.size());
632    for (Pair<Cell, Cell> pair : cellPairs) {
633      resultPairs
634        .add(new Pair<>(pair.getFirst(), createNewCellWithTags(mutation, pair.getSecond())));
635    }
636    return resultPairs;
637  }
638
639  @Override
640  public List<Pair<Cell, Cell>> postAppendBeforeWAL(
641    ObserverContext<RegionCoprocessorEnvironment> ctx, Mutation mutation,
642    List<Pair<Cell, Cell>> cellPairs) throws IOException {
643    List<Pair<Cell, Cell>> resultPairs = new ArrayList<>(cellPairs.size());
644    for (Pair<Cell, Cell> pair : cellPairs) {
645      resultPairs
646        .add(new Pair<>(pair.getFirst(), createNewCellWithTags(mutation, pair.getSecond())));
647    }
648    return resultPairs;
649  }
650
651  private Cell createNewCellWithTags(Mutation mutation, Cell newCell) throws IOException {
652    List<Tag> tags = Lists.newArrayList();
653    CellVisibility cellVisibility = null;
654    try {
655      cellVisibility = mutation.getCellVisibility();
656    } catch (DeserializationException e) {
657      throw new IOException(e);
658    }
659    if (cellVisibility == null) {
660      return newCell;
661    }
662    // Prepend new visibility tags to a new list of tags for the cell
663    // Don't check user auths for labels with Mutations when the user is super user
664    boolean authCheck = authorizationEnabled && checkAuths && !(isSystemOrSuperUser());
665    tags.addAll(this.visibilityLabelService.createVisibilityExpTags(cellVisibility.getExpression(),
666      true, authCheck));
667    // Carry forward all other tags
668    Iterator<Tag> tagsItr = PrivateCellUtil.tagsIterator(newCell);
669    while (tagsItr.hasNext()) {
670      Tag tag = tagsItr.next();
671      if (
672        tag.getType() != TagType.VISIBILITY_TAG_TYPE
673          && tag.getType() != TagType.VISIBILITY_EXP_SERIALIZATION_FORMAT_TAG_TYPE
674      ) {
675        tags.add(tag);
676      }
677    }
678
679    return PrivateCellUtil.createCell(newCell, tags);
680  }
681
682  /******************************
683   * VisibilityEndpoint service related methods
684   ******************************/
685  @Override
686  public synchronized void addLabels(RpcController controller, VisibilityLabelsRequest request,
687    RpcCallback<VisibilityLabelsResponse> done) {
688    VisibilityLabelsResponse.Builder response = VisibilityLabelsResponse.newBuilder();
689    List<VisibilityLabel> visLabels = request.getVisLabelList();
690    if (!initialized) {
691      setExceptionResults(visLabels.size(),
692        new VisibilityControllerNotReadyException("VisibilityController not yet initialized!"),
693        response);
694    } else {
695      List<byte[]> labels = new ArrayList<>(visLabels.size());
696      try {
697        if (authorizationEnabled) {
698          checkCallingUserAuth();
699        }
700        RegionActionResult successResult = RegionActionResult.newBuilder().build();
701        for (VisibilityLabel visLabel : visLabels) {
702          byte[] label = visLabel.getLabel().toByteArray();
703          labels.add(label);
704          response.addResult(successResult); // Just mark as success. Later it will get reset
705                                             // based on the result from
706                                             // visibilityLabelService.addLabels ()
707        }
708        if (!labels.isEmpty()) {
709          OperationStatus[] opStatus = this.visibilityLabelService.addLabels(labels);
710          logResult(true, "addLabels", "Adding labels allowed", null, labels, null);
711          int i = 0;
712          for (OperationStatus status : opStatus) {
713            while (!Objects.equals(response.getResult(i), successResult)) {
714              i++;
715            }
716            if (status.getOperationStatusCode() != SUCCESS) {
717              RegionActionResult.Builder failureResultBuilder = RegionActionResult.newBuilder();
718              failureResultBuilder
719                .setException(buildException(new DoNotRetryIOException(status.getExceptionMsg())));
720              response.setResult(i, failureResultBuilder.build());
721            }
722            i++;
723          }
724        }
725      } catch (AccessDeniedException e) {
726        logResult(false, "addLabels", e.getMessage(), null, labels, null);
727        LOG.error("User is not having required permissions to add labels", e);
728        setExceptionResults(visLabels.size(), e, response);
729      } catch (IOException e) {
730        LOG.error(e.toString(), e);
731        setExceptionResults(visLabels.size(), e, response);
732      }
733    }
734    done.run(response.build());
735  }
736
737  private void setExceptionResults(int size, IOException e,
738    VisibilityLabelsResponse.Builder response) {
739    RegionActionResult.Builder failureResultBuilder = RegionActionResult.newBuilder();
740    failureResultBuilder.setException(buildException(e));
741    RegionActionResult failureResult = failureResultBuilder.build();
742    for (int i = 0; i < size; i++) {
743      response.addResult(i, failureResult);
744    }
745  }
746
747  @Override
748  public synchronized void setAuths(RpcController controller, SetAuthsRequest request,
749    RpcCallback<VisibilityLabelsResponse> done) {
750    VisibilityLabelsResponse.Builder response = VisibilityLabelsResponse.newBuilder();
751    List<ByteString> auths = request.getAuthList();
752    if (!initialized) {
753      setExceptionResults(auths.size(),
754        new VisibilityControllerNotReadyException("VisibilityController not yet initialized!"),
755        response);
756    } else {
757      byte[] user = request.getUser().toByteArray();
758      List<byte[]> labelAuths = new ArrayList<>(auths.size());
759      try {
760        if (authorizationEnabled) {
761          checkCallingUserAuth();
762        }
763        for (ByteString authBS : auths) {
764          labelAuths.add(authBS.toByteArray());
765        }
766        OperationStatus[] opStatus = this.visibilityLabelService.setAuths(user, labelAuths);
767        logResult(true, "setAuths", "Setting authorization for labels allowed", user, labelAuths,
768          null);
769        RegionActionResult successResult = RegionActionResult.newBuilder().build();
770        for (OperationStatus status : opStatus) {
771          if (status.getOperationStatusCode() == SUCCESS) {
772            response.addResult(successResult);
773          } else {
774            RegionActionResult.Builder failureResultBuilder = RegionActionResult.newBuilder();
775            failureResultBuilder
776              .setException(buildException(new DoNotRetryIOException(status.getExceptionMsg())));
777            response.addResult(failureResultBuilder.build());
778          }
779        }
780      } catch (AccessDeniedException e) {
781        logResult(false, "setAuths", e.getMessage(), user, labelAuths, null);
782        LOG.error("User is not having required permissions to set authorization", e);
783        setExceptionResults(auths.size(), e, response);
784      } catch (IOException e) {
785        LOG.error(e.toString(), e);
786        setExceptionResults(auths.size(), e, response);
787      }
788    }
789    done.run(response.build());
790  }
791
792  private void logResult(boolean isAllowed, String request, String reason, byte[] user,
793    List<byte[]> labelAuths, String regex) {
794    if (AUDITLOG.isTraceEnabled()) {
795      // This is more duplicated code!
796      List<String> labelAuthsStr = new ArrayList<>();
797      if (labelAuths != null) {
798        int labelAuthsSize = labelAuths.size();
799        labelAuthsStr = new ArrayList<>(labelAuthsSize);
800        for (int i = 0; i < labelAuthsSize; i++) {
801          labelAuthsStr.add(Bytes.toString(labelAuths.get(i)));
802        }
803      }
804
805      User requestingUser = null;
806      try {
807        requestingUser = VisibilityUtils.getActiveUser();
808      } catch (IOException e) {
809        LOG.warn("Failed to get active system user.");
810        LOG.debug("Details on failure to get active system user.", e);
811      }
812      AUDITLOG.trace("Access " + (isAllowed ? "allowed" : "denied") + " for user "
813        + (requestingUser != null ? requestingUser.getShortName() : "UNKNOWN") + "; reason: "
814        + reason + "; remote address: "
815        + RpcServer.getRemoteAddress().map(InetAddress::toString).orElse("") + "; request: "
816        + request + "; user: " + (user != null ? Bytes.toShort(user) : "null") + "; labels: "
817        + labelAuthsStr + "; regex: " + regex);
818    }
819  }
820
821  @Override
822  public synchronized void getAuths(RpcController controller, GetAuthsRequest request,
823    RpcCallback<GetAuthsResponse> done) {
824    GetAuthsResponse.Builder response = GetAuthsResponse.newBuilder();
825    if (!initialized) {
826      controller.setFailed("VisibilityController not yet initialized");
827    } else {
828      byte[] user = request.getUser().toByteArray();
829      List<String> labels = null;
830      try {
831        // We do ACL check here as we create scanner directly on region. It will not make calls to
832        // AccessController CP methods.
833        if (authorizationEnabled && accessControllerAvailable && !isSystemOrSuperUser()) {
834          User requestingUser = VisibilityUtils.getActiveUser();
835          throw new AccessDeniedException(
836            "User '" + (requestingUser != null ? requestingUser.getShortName() : "null")
837              + "' is not authorized to perform this action.");
838        }
839        if (AuthUtil.isGroupPrincipal(Bytes.toString(user))) {
840          String group = AuthUtil.getGroupName(Bytes.toString(user));
841          labels = this.visibilityLabelService.getGroupAuths(new String[] { group }, false);
842        } else {
843          labels = this.visibilityLabelService.getUserAuths(user, false);
844        }
845        logResult(true, "getAuths", "Get authorizations for user allowed", user, null, null);
846      } catch (AccessDeniedException e) {
847        logResult(false, "getAuths", e.getMessage(), user, null, null);
848        CoprocessorRpcUtils.setControllerException(controller, e);
849      } catch (IOException e) {
850        CoprocessorRpcUtils.setControllerException(controller, e);
851      }
852      response.setUser(request.getUser());
853      if (labels != null) {
854        for (String label : labels) {
855          response.addAuth(ByteString.copyFrom(Bytes.toBytes(label)));
856        }
857      }
858    }
859    done.run(response.build());
860  }
861
862  @Override
863  public synchronized void clearAuths(RpcController controller, SetAuthsRequest request,
864    RpcCallback<VisibilityLabelsResponse> done) {
865    VisibilityLabelsResponse.Builder response = VisibilityLabelsResponse.newBuilder();
866    List<ByteString> auths = request.getAuthList();
867    if (!initialized) {
868      setExceptionResults(auths.size(),
869        new CoprocessorException("VisibilityController not yet initialized"), response);
870    } else {
871      byte[] requestUser = request.getUser().toByteArray();
872      List<byte[]> labelAuths = new ArrayList<>(auths.size());
873      try {
874        // When AC is ON, do AC based user auth check
875        if (authorizationEnabled && accessControllerAvailable && !isSystemOrSuperUser()) {
876          User user = VisibilityUtils.getActiveUser();
877          throw new AccessDeniedException("User '" + (user != null ? user.getShortName() : "null")
878            + " is not authorized to perform this action.");
879        }
880        if (authorizationEnabled) {
881          checkCallingUserAuth(); // When AC is not in place the calling user should have
882                                  // SYSTEM_LABEL auth to do this action.
883        }
884        for (ByteString authBS : auths) {
885          labelAuths.add(authBS.toByteArray());
886        }
887
888        OperationStatus[] opStatus =
889          this.visibilityLabelService.clearAuths(requestUser, labelAuths);
890        logResult(true, "clearAuths", "Removing authorization for labels allowed", requestUser,
891          labelAuths, null);
892        RegionActionResult successResult = RegionActionResult.newBuilder().build();
893        for (OperationStatus status : opStatus) {
894          if (status.getOperationStatusCode() == SUCCESS) {
895            response.addResult(successResult);
896          } else {
897            RegionActionResult.Builder failureResultBuilder = RegionActionResult.newBuilder();
898            failureResultBuilder
899              .setException(buildException(new DoNotRetryIOException(status.getExceptionMsg())));
900            response.addResult(failureResultBuilder.build());
901          }
902        }
903      } catch (AccessDeniedException e) {
904        logResult(false, "clearAuths", e.getMessage(), requestUser, labelAuths, null);
905        LOG.error("User is not having required permissions to clear authorization", e);
906        setExceptionResults(auths.size(), e, response);
907      } catch (IOException e) {
908        LOG.error(e.toString(), e);
909        setExceptionResults(auths.size(), e, response);
910      }
911    }
912    done.run(response.build());
913  }
914
915  @Override
916  public synchronized void listLabels(RpcController controller, ListLabelsRequest request,
917    RpcCallback<ListLabelsResponse> done) {
918    ListLabelsResponse.Builder response = ListLabelsResponse.newBuilder();
919    if (!initialized) {
920      controller.setFailed("VisibilityController not yet initialized");
921    } else {
922      List<String> labels = null;
923      String regex = request.hasRegex() ? request.getRegex() : null;
924      try {
925        // We do ACL check here as we create scanner directly on region. It will not make calls to
926        // AccessController CP methods.
927        if (authorizationEnabled && accessControllerAvailable && !isSystemOrSuperUser()) {
928          User requestingUser = VisibilityUtils.getActiveUser();
929          throw new AccessDeniedException(
930            "User '" + (requestingUser != null ? requestingUser.getShortName() : "null")
931              + "' is not authorized to perform this action.");
932        }
933        labels = this.visibilityLabelService.listLabels(regex);
934        logResult(true, "listLabels", "Listing labels allowed", null, null, regex);
935      } catch (AccessDeniedException e) {
936        logResult(false, "listLabels", e.getMessage(), null, null, regex);
937        CoprocessorRpcUtils.setControllerException(controller, e);
938      } catch (IOException e) {
939        CoprocessorRpcUtils.setControllerException(controller, e);
940      }
941      if (labels != null && !labels.isEmpty()) {
942        for (String label : labels) {
943          response.addLabel(ByteString.copyFrom(Bytes.toBytes(label)));
944        }
945      }
946    }
947    done.run(response.build());
948  }
949
950  private void checkCallingUserAuth() throws IOException {
951    if (!authorizationEnabled) { // Redundant, but just in case
952      return;
953    }
954    if (!accessControllerAvailable) {
955      User user = VisibilityUtils.getActiveUser();
956      if (user == null) {
957        throw new IOException("Unable to retrieve calling user");
958      }
959      if (!(this.visibilityLabelService.havingSystemAuth(user))) {
960        throw new AccessDeniedException(
961          "User '" + user.getShortName() + "' is not authorized to perform this action.");
962      }
963    }
964  }
965
966  private static class DeleteVersionVisibilityExpressionFilter extends FilterBase {
967    private List<Tag> deleteCellVisTags;
968    private Byte deleteCellVisTagsFormat;
969
970    public DeleteVersionVisibilityExpressionFilter(List<Tag> deleteCellVisTags,
971      Byte deleteCellVisTagsFormat) {
972      this.deleteCellVisTags = deleteCellVisTags;
973      this.deleteCellVisTagsFormat = deleteCellVisTagsFormat;
974    }
975
976    @Override
977    public boolean filterRowKey(Cell cell) throws IOException {
978      // Impl in FilterBase might do unnecessary copy for Off heap backed Cells.
979      return false;
980    }
981
982    @Override
983    public ReturnCode filterCell(final Cell cell) throws IOException {
984      List<Tag> putVisTags = new ArrayList<>();
985      Byte putCellVisTagsFormat = VisibilityUtils.extractVisibilityTags(cell, putVisTags);
986      if (putVisTags.isEmpty() && deleteCellVisTags.isEmpty()) {
987        // Early out if there are no tags in the cell
988        return ReturnCode.INCLUDE;
989      }
990      boolean matchFound =
991        VisibilityLabelServiceManager.getInstance().getVisibilityLabelService().matchVisibility(
992          putVisTags, putCellVisTagsFormat, deleteCellVisTags, deleteCellVisTagsFormat);
993      return matchFound ? ReturnCode.INCLUDE : ReturnCode.SKIP;
994    }
995
996    @Override
997    public boolean equals(Object obj) {
998      if (!(obj instanceof DeleteVersionVisibilityExpressionFilter)) {
999        return false;
1000      }
1001      if (this == obj) {
1002        return true;
1003      }
1004      DeleteVersionVisibilityExpressionFilter f = (DeleteVersionVisibilityExpressionFilter) obj;
1005      return this.deleteCellVisTags.equals(f.deleteCellVisTags)
1006        && this.deleteCellVisTagsFormat.equals(f.deleteCellVisTagsFormat);
1007    }
1008
1009    @Override
1010    public int hashCode() {
1011      return Objects.hash(this.deleteCellVisTags, this.deleteCellVisTagsFormat);
1012    }
1013  }
1014
1015  /** Returns NameValuePair of the exception name to stringified version os exception. */
1016  // Copied from ResponseConverter and made private. Only used in here.
1017  private static NameBytesPair buildException(final Throwable t) {
1018    NameBytesPair.Builder parameterBuilder = NameBytesPair.newBuilder();
1019    parameterBuilder.setName(t.getClass().getName());
1020    parameterBuilder.setValue(ByteString.copyFromUtf8(StringUtils.stringifyException(t)));
1021    return parameterBuilder.build();
1022  }
1023}