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.TagType.VISIBILITY_TAG_TYPE;
021import static org.apache.hadoop.hbase.security.visibility.VisibilityConstants.LABELS_TABLE_FAMILY;
022import static org.apache.hadoop.hbase.security.visibility.VisibilityConstants.LABELS_TABLE_NAME;
023import static org.apache.hadoop.hbase.security.visibility.VisibilityUtils.SYSTEM_LABEL;
024
025import java.io.ByteArrayOutputStream;
026import java.io.DataOutputStream;
027import java.io.IOException;
028import java.util.ArrayList;
029import java.util.Collections;
030import java.util.HashSet;
031import java.util.Iterator;
032import java.util.List;
033import java.util.Set;
034import org.apache.hadoop.conf.Configuration;
035import org.apache.hadoop.hbase.ArrayBackedTag;
036import org.apache.hadoop.hbase.AuthUtil;
037import org.apache.hadoop.hbase.Cell;
038import org.apache.hadoop.hbase.CellBuilder;
039import org.apache.hadoop.hbase.CellBuilderFactory;
040import org.apache.hadoop.hbase.CellBuilderType;
041import org.apache.hadoop.hbase.HConstants.OperationStatusCode;
042import org.apache.hadoop.hbase.PrivateCellUtil;
043import org.apache.hadoop.hbase.Tag;
044import org.apache.hadoop.hbase.TagType;
045import org.apache.hadoop.hbase.client.Connection;
046import org.apache.hadoop.hbase.client.ConnectionFactory;
047import org.apache.hadoop.hbase.client.Delete;
048import org.apache.hadoop.hbase.client.Get;
049import org.apache.hadoop.hbase.client.Put;
050import org.apache.hadoop.hbase.client.Result;
051import org.apache.hadoop.hbase.client.Scan;
052import org.apache.hadoop.hbase.client.Table;
053import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
054import org.apache.hadoop.hbase.regionserver.OperationStatus;
055import org.apache.hadoop.hbase.regionserver.Region;
056import org.apache.hadoop.hbase.regionserver.RegionScanner;
057import org.apache.hadoop.hbase.security.Superusers;
058import org.apache.hadoop.hbase.security.User;
059import org.apache.hadoop.hbase.security.visibility.expression.ExpressionNode;
060import org.apache.hadoop.hbase.security.visibility.expression.LeafExpressionNode;
061import org.apache.hadoop.hbase.security.visibility.expression.NonLeafExpressionNode;
062import org.apache.hadoop.hbase.security.visibility.expression.Operator;
063import org.apache.hadoop.hbase.util.ByteBufferUtils;
064import org.apache.hadoop.hbase.util.Bytes;
065import org.apache.yetus.audience.InterfaceAudience;
066import org.slf4j.Logger;
067import org.slf4j.LoggerFactory;
068
069/**
070 * This is a VisibilityLabelService where labels in Mutation's visibility expression will be
071 * persisted as Strings itself rather than ordinals in 'labels' table. Also there is no need to add
072 * labels to the system, prior to using them in Mutations/Authorizations.
073 */
074@InterfaceAudience.Private
075public class ExpAsStringVisibilityLabelServiceImpl implements VisibilityLabelService {
076  private static final Logger LOG =
077    LoggerFactory.getLogger(ExpAsStringVisibilityLabelServiceImpl.class);
078
079  private static final byte[] DUMMY_VALUE = new byte[0];
080  private static final byte STRING_SERIALIZATION_FORMAT = 2;
081  private static final Tag STRING_SERIALIZATION_FORMAT_TAG =
082    new ArrayBackedTag(TagType.VISIBILITY_EXP_SERIALIZATION_FORMAT_TAG_TYPE,
083      new byte[] { STRING_SERIALIZATION_FORMAT });
084  private final ExpressionParser expressionParser = new ExpressionParser();
085  private final ExpressionExpander expressionExpander = new ExpressionExpander();
086  private Configuration conf;
087  private Region labelsRegion;
088  private List<ScanLabelGenerator> scanLabelGenerators;
089
090  @Override
091  public OperationStatus[] addLabels(List<byte[]> labels) throws IOException {
092    // Not doing specific label add. We will just add labels in Mutation
093    // visibility expression as it
094    // is along with every cell.
095    OperationStatus[] status = new OperationStatus[labels.size()];
096    for (int i = 0; i < labels.size(); i++) {
097      status[i] = new OperationStatus(OperationStatusCode.SUCCESS);
098    }
099    return status;
100  }
101
102  @Override
103  public OperationStatus[] setAuths(byte[] user, List<byte[]> authLabels) throws IOException {
104    assert labelsRegion != null;
105    OperationStatus[] finalOpStatus = new OperationStatus[authLabels.size()];
106    Put p = new Put(user);
107    CellBuilder builder = CellBuilderFactory.create(CellBuilderType.SHALLOW_COPY);
108    for (byte[] auth : authLabels) {
109      p.add(builder.clear().setRow(p.getRow()).setFamily(LABELS_TABLE_FAMILY).setQualifier(auth)
110        .setTimestamp(p.getTimestamp()).setType(Cell.Type.Put).setValue(DUMMY_VALUE).build());
111    }
112    this.labelsRegion.put(p);
113    // This is a testing impl and so not doing any caching
114    for (int i = 0; i < authLabels.size(); i++) {
115      finalOpStatus[i] = new OperationStatus(OperationStatusCode.SUCCESS);
116    }
117    return finalOpStatus;
118  }
119
120  @Override
121  public OperationStatus[] clearAuths(byte[] user, List<byte[]> authLabels) throws IOException {
122    assert labelsRegion != null;
123    OperationStatus[] finalOpStatus = new OperationStatus[authLabels.size()];
124    List<String> currentAuths;
125    if (AuthUtil.isGroupPrincipal(Bytes.toString(user))) {
126      String group = AuthUtil.getGroupName(Bytes.toString(user));
127      currentAuths = this.getGroupAuths(new String[] { group }, true);
128    } else {
129      currentAuths = this.getUserAuths(user, true);
130    }
131    Delete d = new Delete(user);
132    int i = 0;
133    for (byte[] authLabel : authLabels) {
134      String authLabelStr = Bytes.toString(authLabel);
135      if (currentAuths.contains(authLabelStr)) {
136        d.addColumns(LABELS_TABLE_FAMILY, authLabel);
137      } else {
138        // This label is not set for the user.
139        finalOpStatus[i] =
140          new OperationStatus(OperationStatusCode.FAILURE, new InvalidLabelException(
141            "Label '" + authLabelStr + "' is not set for the user " + Bytes.toString(user)));
142      }
143      i++;
144    }
145    this.labelsRegion.delete(d);
146    // This is a testing impl and so not doing any caching
147    for (i = 0; i < authLabels.size(); i++) {
148      if (finalOpStatus[i] == null) {
149        finalOpStatus[i] = new OperationStatus(OperationStatusCode.SUCCESS);
150      }
151    }
152    return finalOpStatus;
153  }
154
155  @Override
156  public List<String> getUserAuths(byte[] user, boolean systemCall) throws IOException {
157    assert (labelsRegion != null || systemCall);
158    List<String> auths = new ArrayList<>();
159    Get get = new Get(user);
160    getAuths(get, auths);
161    return auths;
162  }
163
164  @Override
165  public List<String> getGroupAuths(String[] groups, boolean systemCall) throws IOException {
166    assert (labelsRegion != null || systemCall);
167    List<String> auths = new ArrayList<>();
168    if (groups != null && groups.length > 0) {
169      for (String group : groups) {
170        Get get = new Get(Bytes.toBytes(AuthUtil.toGroupEntry(group)));
171        getAuths(get, auths);
172      }
173    }
174    return auths;
175  }
176
177  private void getAuths(Get get, List<String> auths) throws IOException {
178    List<Cell> cells = new ArrayList<>();
179    RegionScanner scanner = null;
180    try {
181      if (labelsRegion == null) {
182        Table table = null;
183        Connection connection = null;
184        try {
185          connection = ConnectionFactory.createConnection(conf);
186          table = connection.getTable(VisibilityConstants.LABELS_TABLE_NAME);
187          Result result = table.get(get);
188          cells = result.listCells();
189        } finally {
190          if (table != null) {
191            table.close();
192          }
193          if (connection != null) {
194            connection.close();
195          }
196        }
197      } else {
198        // NOTE: Please don't use HRegion.get() instead,
199        // because it will copy cells to heap. See HBASE-26036
200        scanner = this.labelsRegion.getScanner(new Scan(get));
201        scanner.next(cells);
202      }
203      for (Cell cell : cells) {
204        String auth = Bytes.toString(cell.getQualifierArray(), cell.getQualifierOffset(),
205          cell.getQualifierLength());
206        auths.add(auth);
207      }
208    } finally {
209      if (scanner != null) {
210        scanner.close();
211      }
212    }
213  }
214
215  @Override
216  public List<String> listLabels(String regex) throws IOException {
217    // return an empty list for this implementation.
218    return new ArrayList<>();
219  }
220
221  @Override
222  public List<Tag> createVisibilityExpTags(String visExpression, boolean withSerializationFormat,
223    boolean checkAuths) throws IOException {
224    ExpressionNode node = null;
225    try {
226      node = this.expressionParser.parse(visExpression);
227    } catch (ParseException e) {
228      throw new IOException(e);
229    }
230    node = this.expressionExpander.expand(node);
231    List<Tag> tags = new ArrayList<>();
232    if (withSerializationFormat) {
233      tags.add(STRING_SERIALIZATION_FORMAT_TAG);
234    }
235    if (
236      node instanceof NonLeafExpressionNode
237        && ((NonLeafExpressionNode) node).getOperator() == Operator.OR
238    ) {
239      for (ExpressionNode child : ((NonLeafExpressionNode) node).getChildExps()) {
240        tags.add(createTag(child));
241      }
242    } else {
243      tags.add(createTag(node));
244    }
245    return tags;
246  }
247
248  @Override
249  public VisibilityExpEvaluator getVisibilityExpEvaluator(Authorizations authorizations)
250    throws IOException {
251    // If a super user issues a get/scan, he should be able to scan the cells
252    // irrespective of the Visibility labels
253    if (isReadFromSystemAuthUser()) {
254      return new VisibilityExpEvaluator() {
255        @Override
256        public boolean evaluate(Cell cell) throws IOException {
257          return true;
258        }
259      };
260    }
261    List<String> authLabels = null;
262    for (ScanLabelGenerator scanLabelGenerator : scanLabelGenerators) {
263      try {
264        // null authorizations to be handled inside SLG impl.
265        authLabels = scanLabelGenerator.getLabels(VisibilityUtils.getActiveUser(), authorizations);
266        authLabels = (authLabels == null) ? new ArrayList<>() : authLabels;
267        authorizations = new Authorizations(authLabels);
268      } catch (Throwable t) {
269        LOG.error(t.toString(), t);
270        throw new IOException(t);
271      }
272    }
273    final List<String> authLabelsFinal = authLabels;
274    return new VisibilityExpEvaluator() {
275      @Override
276      public boolean evaluate(Cell cell) throws IOException {
277        boolean visibilityTagPresent = false;
278        // Save an object allocation where we can
279        if (cell.getTagsLength() > 0) {
280          Iterator<Tag> tagsItr = PrivateCellUtil.tagsIterator(cell);
281          while (tagsItr.hasNext()) {
282            boolean includeKV = true;
283            Tag tag = tagsItr.next();
284            if (tag.getType() == VISIBILITY_TAG_TYPE) {
285              visibilityTagPresent = true;
286              int offset = tag.getValueOffset();
287              int endOffset = offset + tag.getValueLength();
288              while (offset < endOffset) {
289                short len = getTagValuePartAsShort(tag, offset);
290                offset += 2;
291                if (len < 0) {
292                  // This is a NOT label.
293                  len = (short) (-1 * len);
294                  String label = getTagValuePartAsString(tag, offset, len);
295                  if (authLabelsFinal.contains(label)) {
296                    includeKV = false;
297                    break;
298                  }
299                } else {
300                  String label = getTagValuePartAsString(tag, offset, len);
301                  if (!authLabelsFinal.contains(label)) {
302                    includeKV = false;
303                    break;
304                  }
305                }
306                offset += len;
307              }
308              if (includeKV) {
309                // We got one visibility expression getting evaluated to true.
310                // Good to include this
311                // KV in the result then.
312                return true;
313              }
314            }
315          }
316        }
317        return !(visibilityTagPresent);
318      }
319    };
320  }
321
322  protected boolean isReadFromSystemAuthUser() throws IOException {
323    User user = VisibilityUtils.getActiveUser();
324    return havingSystemAuth(user);
325  }
326
327  private Tag createTag(ExpressionNode node) throws IOException {
328    ByteArrayOutputStream baos = new ByteArrayOutputStream();
329    DataOutputStream dos = new DataOutputStream(baos);
330    List<String> labels = new ArrayList<>();
331    List<String> notLabels = new ArrayList<>();
332    extractLabels(node, labels, notLabels);
333    Collections.sort(labels);
334    Collections.sort(notLabels);
335    // We will write the NOT labels 1st followed by normal labels
336    // Each of the label we will write with label length (as short 1st) followed
337    // by the label bytes.
338    // For a NOT node we will write the label length as -ve.
339    for (String label : notLabels) {
340      byte[] bLabel = Bytes.toBytes(label);
341      short length = (short) bLabel.length;
342      length = (short) (-1 * length);
343      dos.writeShort(length);
344      dos.write(bLabel);
345    }
346    for (String label : labels) {
347      byte[] bLabel = Bytes.toBytes(label);
348      dos.writeShort(bLabel.length);
349      dos.write(bLabel);
350    }
351    return new ArrayBackedTag(VISIBILITY_TAG_TYPE, baos.toByteArray());
352  }
353
354  private void extractLabels(ExpressionNode node, List<String> labels, List<String> notLabels) {
355    if (node.isSingleNode()) {
356      if (node instanceof NonLeafExpressionNode) {
357        // This is a NOT node.
358        LeafExpressionNode lNode =
359          (LeafExpressionNode) ((NonLeafExpressionNode) node).getChildExps().get(0);
360        notLabels.add(lNode.getIdentifier());
361      } else {
362        labels.add(((LeafExpressionNode) node).getIdentifier());
363      }
364    } else {
365      // A non leaf expression of labels with & operator.
366      NonLeafExpressionNode nlNode = (NonLeafExpressionNode) node;
367      assert nlNode.getOperator() == Operator.AND;
368      List<ExpressionNode> childExps = nlNode.getChildExps();
369      for (ExpressionNode child : childExps) {
370        extractLabels(child, labels, notLabels);
371      }
372    }
373  }
374
375  @Override
376  public Configuration getConf() {
377    return this.conf;
378  }
379
380  @Override
381  public void setConf(Configuration conf) {
382    this.conf = conf;
383  }
384
385  @Override
386  public void init(RegionCoprocessorEnvironment e) throws IOException {
387    this.scanLabelGenerators = VisibilityUtils.getScanLabelGenerators(this.conf);
388    if (e.getRegion().getRegionInfo().getTable().equals(LABELS_TABLE_NAME)) {
389      this.labelsRegion = e.getRegion();
390    }
391  }
392
393  @Override
394  public boolean havingSystemAuth(User user) throws IOException {
395    if (Superusers.isSuperUser(user)) {
396      return true;
397    }
398    Set<String> auths = new HashSet<>();
399    auths.addAll(this.getUserAuths(Bytes.toBytes(user.getShortName()), true));
400    auths.addAll(this.getGroupAuths(user.getGroupNames(), true));
401    return auths.contains(SYSTEM_LABEL);
402  }
403
404  @Override
405  public boolean matchVisibility(List<Tag> putTags, Byte putTagsFormat, List<Tag> deleteTags,
406    Byte deleteTagsFormat) throws IOException {
407    assert putTagsFormat == STRING_SERIALIZATION_FORMAT;
408    assert deleteTagsFormat == STRING_SERIALIZATION_FORMAT;
409    return checkForMatchingVisibilityTagsWithSortedOrder(putTags, deleteTags);
410  }
411
412  private static boolean checkForMatchingVisibilityTagsWithSortedOrder(List<Tag> putVisTags,
413    List<Tag> deleteVisTags) {
414    // Early out if there are no tags in both of cell and delete
415    if (putVisTags.isEmpty() && deleteVisTags.isEmpty()) {
416      return true;
417    }
418    boolean matchFound = false;
419    // If the size does not match. Definitely we are not comparing the equal
420    // tags.
421    if ((deleteVisTags.size()) == putVisTags.size()) {
422      for (Tag tag : deleteVisTags) {
423        matchFound = false;
424        for (Tag givenTag : putVisTags) {
425          if (Tag.matchingValue(tag, givenTag)) {
426            matchFound = true;
427            break;
428          }
429        }
430        if (!matchFound) break;
431      }
432    }
433    return matchFound;
434  }
435
436  @Override
437  public byte[] encodeVisibilityForReplication(final List<Tag> tags, final Byte serializationFormat)
438    throws IOException {
439    if (
440      tags.size() > 0
441        && (serializationFormat == null || serializationFormat == STRING_SERIALIZATION_FORMAT)
442    ) {
443      return createModifiedVisExpression(tags);
444    }
445    return null;
446  }
447
448  /**
449   * @param tags - all the tags associated with the current Cell
450   * @return - the modified visibility expression as byte[]
451   */
452  private byte[] createModifiedVisExpression(final List<Tag> tags) throws IOException {
453    StringBuilder visibilityString = new StringBuilder();
454    for (Tag tag : tags) {
455      if (tag.getType() == TagType.VISIBILITY_TAG_TYPE) {
456        if (visibilityString.length() != 0) {
457          visibilityString
458            .append(VisibilityConstants.CLOSED_PARAN + VisibilityConstants.OR_OPERATOR);
459        }
460        int offset = tag.getValueOffset();
461        int endOffset = offset + tag.getValueLength();
462        boolean expressionStart = true;
463        while (offset < endOffset) {
464          short len = getTagValuePartAsShort(tag, offset);
465          offset += 2;
466          if (len < 0) {
467            len = (short) (-1 * len);
468            String label = getTagValuePartAsString(tag, offset, len);
469            if (expressionStart) {
470              visibilityString.append(VisibilityConstants.OPEN_PARAN
471                + VisibilityConstants.NOT_OPERATOR + CellVisibility.quote(label));
472            } else {
473              visibilityString.append(VisibilityConstants.AND_OPERATOR
474                + VisibilityConstants.NOT_OPERATOR + CellVisibility.quote(label));
475            }
476          } else {
477            String label = getTagValuePartAsString(tag, offset, len);
478            if (expressionStart) {
479              visibilityString.append(VisibilityConstants.OPEN_PARAN + CellVisibility.quote(label));
480            } else {
481              visibilityString
482                .append(VisibilityConstants.AND_OPERATOR + CellVisibility.quote(label));
483            }
484          }
485          expressionStart = false;
486          offset += len;
487        }
488      }
489    }
490    if (visibilityString.length() != 0) {
491      visibilityString.append(VisibilityConstants.CLOSED_PARAN);
492      // Return the string formed as byte[]
493      return Bytes.toBytes(visibilityString.toString());
494    }
495    return null;
496  }
497
498  private static short getTagValuePartAsShort(Tag t, int offset) {
499    if (t.hasArray()) {
500      return Bytes.toShort(t.getValueArray(), offset);
501    }
502    return ByteBufferUtils.toShort(t.getValueByteBuffer(), offset);
503  }
504
505  private static String getTagValuePartAsString(Tag t, int offset, int length) {
506    if (t.hasArray()) {
507      return Bytes.toString(t.getValueArray(), offset, length);
508    }
509    byte[] b = new byte[length];
510    ByteBufferUtils.copyFromBufferToArray(b, t.getValueByteBuffer(), offset, 0, length);
511    return Bytes.toString(b);
512  }
513}