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