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 * <p/>
010 * http://www.apache.org/licenses/LICENSE-2.0
011 * <p/>
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.test;
019
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertTrue;
022
023import java.io.IOException;
024import java.security.PrivilegedExceptionAction;
025import java.util.List;
026
027import org.apache.hadoop.conf.Configuration;
028import org.apache.hadoop.fs.Path;
029import org.apache.hadoop.hbase.HBaseConfiguration;
030import org.apache.hadoop.hbase.HColumnDescriptor;
031import org.apache.hadoop.hbase.HConstants;
032import org.apache.hadoop.hbase.HTableDescriptor;
033import org.apache.hadoop.hbase.IntegrationTestingUtility;
034import org.apache.hadoop.hbase.client.Admin;
035import org.apache.hadoop.hbase.client.Connection;
036import org.apache.hadoop.hbase.client.ConnectionFactory;
037import org.apache.hadoop.hbase.client.Put;
038import org.apache.hadoop.hbase.client.Result;
039import org.apache.hadoop.hbase.client.Scan;
040import org.apache.hadoop.hbase.client.ScannerCallable;
041import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
042import org.apache.hadoop.hbase.mapreduce.TableMapReduceUtil;
043import org.apache.hadoop.hbase.mapreduce.TableMapper;
044import org.apache.hadoop.hbase.mapreduce.TableRecordReaderImpl;
045import org.apache.hadoop.hbase.security.User;
046import org.apache.hadoop.hbase.security.visibility.Authorizations;
047import org.apache.hadoop.hbase.security.visibility.CellVisibility;
048import org.apache.hadoop.hbase.security.visibility.VisibilityClient;
049import org.apache.hadoop.hbase.security.visibility.VisibilityTestUtil;
050import org.apache.hadoop.hbase.testclassification.IntegrationTests;
051import org.apache.hadoop.hbase.util.AbstractHBaseTool;
052import org.apache.hadoop.hbase.util.Bytes;
053import org.apache.hadoop.io.BytesWritable;
054import org.apache.hadoop.io.NullWritable;
055import org.apache.hadoop.mapreduce.Counter;
056import org.apache.hadoop.mapreduce.Job;
057import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
058import org.apache.hadoop.util.ToolRunner;
059import org.junit.experimental.categories.Category;
060
061import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine;
062
063/**
064 * A large test which loads a lot of data with cell visibility, and verifies the data. Test adds 2
065 * users with different sets of visibility labels authenticated for them. Every row (so cells in
066 * that) added with visibility expressions. In load step, 200 map tasks are launched, which in turn
067 * write loadmapper.num_to_write (default 100K) rows to an hbase table. Rows are written in blocks,
068 * for a total of 100 blocks.
069 *
070 * Verify step scans the table as both users with Authorizations. This step asserts that user can
071 * see only those rows (and so cells) with visibility for which they have label auth.
072 *
073 * This class can be run as a unit test, as an integration test, or from the command line.
074 *
075 * Originally taken from Apache Bigtop.
076 * Issue user names as comma seperated list.
077 *./hbase IntegrationTestWithCellVisibilityLoadAndVerify -u usera,userb
078 */
079@Category(IntegrationTests.class)
080public class IntegrationTestWithCellVisibilityLoadAndVerify extends IntegrationTestLoadAndVerify {
081  private static final String ERROR_STR =
082      "Two user names are to be specified seperated by a ',' like 'usera,userb'";
083  private static final char NOT = '!';
084  private static final char OR = '|';
085  private static final char AND = '&';
086  private static final String TEST_NAME = "IntegrationTestCellVisibilityLoadAndVerify";
087  private static final String CONFIDENTIAL = "confidential";
088  private static final String TOPSECRET = "topsecret";
089  private static final String SECRET = "secret";
090  private static final String PUBLIC = "public";
091  private static final String PRIVATE = "private";
092  private static final String[] LABELS = { CONFIDENTIAL, TOPSECRET, SECRET, PRIVATE, PUBLIC };
093  private static final String[] VISIBILITY_EXPS = { CONFIDENTIAL + AND + TOPSECRET + AND + PRIVATE,
094      CONFIDENTIAL + OR + TOPSECRET, PUBLIC,
095      '(' + SECRET + OR + PRIVATE + ')' + AND + NOT + CONFIDENTIAL };
096  private static final int VISIBILITY_EXPS_COUNT = VISIBILITY_EXPS.length;
097  private static final byte[] TEST_FAMILY = Bytes.toBytes("f1");
098  private static final byte[] TEST_QUALIFIER = Bytes.toBytes("q1");
099  private static final String NUM_TO_WRITE_KEY = "loadmapper.num_to_write";
100  private static final long NUM_TO_WRITE_DEFAULT = 100 * 1000;
101  private static final int SCANNER_CACHING = 500;
102  private static String USER_OPT = "users";
103  private static String userNames = "user1,user2";
104
105  private long numRowsLoadedWithExp1, numRowsLoadedWithExp2, numRowsLoadWithExp3,
106      numRowsLoadWithExp4;
107  private long numRowsReadWithExp1, numRowsReadWithExp2, numRowsReadWithExp3, numRowsReadWithExp4;
108
109  private static User USER1, USER2;
110
111  private enum Counters {
112    ROWS_VIS_EXP_1, ROWS_VIS_EXP_2, ROWS_VIS_EXP_3, ROWS_VIS_EXP_4;
113  }
114
115  @Override
116  public void setUpCluster() throws Exception {
117    util = getTestingUtil(null);
118    Configuration conf = util.getConfiguration();
119    VisibilityTestUtil.enableVisiblityLabels(conf);
120    conf.set("hbase.superuser", User.getCurrent().getName());
121    conf.setBoolean("dfs.permissions", false);
122    super.setUpCluster();
123    String[] users = userNames.split(",");
124    if (users.length != 2) {
125      System.err.println(ERROR_STR);
126      throw new IOException(ERROR_STR);
127    }
128    System.out.println(userNames + " "+users[0]+ " "+users[1]);
129    USER1 = User.createUserForTesting(conf, users[0], new String[] {});
130    USER2 = User.createUserForTesting(conf, users[1], new String[] {});
131    addLabelsAndAuths();
132  }
133
134  @Override
135  protected void addOptions() {
136    super.addOptions();
137    addOptWithArg("u", USER_OPT, "User names to be passed");
138  }
139
140  private void addLabelsAndAuths() throws Exception {
141    try {
142      VisibilityClient.addLabels(util.getConnection(), LABELS);
143      VisibilityClient.setAuths(util.getConnection(), new String[] { CONFIDENTIAL, TOPSECRET,
144          SECRET, PRIVATE }, USER1.getName());
145      VisibilityClient.setAuths(util.getConnection(), new String[] { PUBLIC },
146          USER2.getName());
147    } catch (Throwable t) {
148      throw new IOException(t);
149    }
150  }
151
152  public static class LoadWithCellVisibilityMapper extends LoadMapper {
153    private Counter rowsExp1, rowsExp2, rowsExp3, rowsexp4;
154
155    @Override
156    public void setup(Context context) throws IOException {
157      super.setup(context);
158      rowsExp1 = context.getCounter(Counters.ROWS_VIS_EXP_1);
159      rowsExp2 = context.getCounter(Counters.ROWS_VIS_EXP_2);
160      rowsExp3 = context.getCounter(Counters.ROWS_VIS_EXP_3);
161      rowsexp4 = context.getCounter(Counters.ROWS_VIS_EXP_4);
162    }
163
164    @Override
165    protected void map(NullWritable key, NullWritable value, Context context) throws IOException,
166        InterruptedException {
167      String suffix = "/" + shortTaskId;
168      int BLOCK_SIZE = (int) (recordsToWrite / 100);
169      for (long i = 0; i < recordsToWrite;) {
170        for (long idx = 0; idx < BLOCK_SIZE && i < recordsToWrite; idx++, i++) {
171          int expIdx = rand.nextInt(BLOCK_SIZE) % VISIBILITY_EXPS_COUNT;
172          String exp = VISIBILITY_EXPS[expIdx];
173          byte[] row = Bytes.add(Bytes.toBytes(i), Bytes.toBytes(suffix), Bytes.toBytes(exp));
174          Put p = new Put(row);
175          p.addColumn(TEST_FAMILY, TEST_QUALIFIER, HConstants.EMPTY_BYTE_ARRAY);
176          p.setCellVisibility(new CellVisibility(exp));
177          getCounter(expIdx).increment(1);
178          mutator.mutate(p);
179
180          if (i % 100 == 0) {
181            context.setStatus("Written " + i + "/" + recordsToWrite + " records");
182            context.progress();
183          }
184        }
185        // End of block, flush all of them before we start writing anything
186        // pointing to these!
187        mutator.flush();
188      }
189    }
190
191    private Counter getCounter(int idx) {
192      switch (idx) {
193      case 0:
194        return rowsExp1;
195      case 1:
196        return rowsExp2;
197      case 2:
198        return rowsExp3;
199      case 3:
200        return rowsexp4;
201      default:
202        return null;
203      }
204    }
205  }
206
207  public static class VerifyMapper extends TableMapper<BytesWritable, BytesWritable> {
208    private Counter rowsExp1, rowsExp2, rowsExp3, rowsExp4;
209
210    @Override
211    public void setup(Context context) throws IOException {
212      rowsExp1 = context.getCounter(Counters.ROWS_VIS_EXP_1);
213      rowsExp2 = context.getCounter(Counters.ROWS_VIS_EXP_2);
214      rowsExp3 = context.getCounter(Counters.ROWS_VIS_EXP_3);
215      rowsExp4 = context.getCounter(Counters.ROWS_VIS_EXP_4);
216    }
217
218    @Override
219    protected void map(ImmutableBytesWritable key, Result value, Context context)
220        throws IOException, InterruptedException {
221      byte[] row = value.getRow();
222      Counter c = getCounter(row);
223      c.increment(1);
224    }
225
226    private Counter getCounter(byte[] row) {
227      Counter c = null;
228      if (Bytes.indexOf(row, Bytes.toBytes(VISIBILITY_EXPS[0])) != -1) {
229        c = rowsExp1;
230      } else if (Bytes.indexOf(row, Bytes.toBytes(VISIBILITY_EXPS[1])) != -1) {
231        c = rowsExp2;
232      } else if (Bytes.indexOf(row, Bytes.toBytes(VISIBILITY_EXPS[2])) != -1) {
233        c = rowsExp3;
234      } else if (Bytes.indexOf(row, Bytes.toBytes(VISIBILITY_EXPS[3])) != -1) {
235        c = rowsExp4;
236      }
237      return c;
238    }
239  }
240
241  @Override
242  protected Job doLoad(Configuration conf, HTableDescriptor htd) throws Exception {
243    Job job = super.doLoad(conf, htd);
244    this.numRowsLoadedWithExp1 = job.getCounters().findCounter(Counters.ROWS_VIS_EXP_1).getValue();
245    this.numRowsLoadedWithExp2 = job.getCounters().findCounter(Counters.ROWS_VIS_EXP_2).getValue();
246    this.numRowsLoadWithExp3 = job.getCounters().findCounter(Counters.ROWS_VIS_EXP_3).getValue();
247    this.numRowsLoadWithExp4 = job.getCounters().findCounter(Counters.ROWS_VIS_EXP_4).getValue();
248    System.out.println("Rows loaded with cell visibility " + VISIBILITY_EXPS[0] + " : "
249        + this.numRowsLoadedWithExp1);
250    System.out.println("Rows loaded with cell visibility " + VISIBILITY_EXPS[1] + " : "
251        + this.numRowsLoadedWithExp2);
252    System.out.println("Rows loaded with cell visibility " + VISIBILITY_EXPS[2] + " : "
253        + this.numRowsLoadWithExp3);
254    System.out.println("Rows loaded with cell visibility " + VISIBILITY_EXPS[3] + " : "
255        + this.numRowsLoadWithExp4);
256    return job;
257  }
258
259  @Override
260  protected void setMapperClass(Job job) {
261    job.setMapperClass(LoadWithCellVisibilityMapper.class);
262  }
263
264  @Override
265  protected void doVerify(final Configuration conf, final HTableDescriptor htd) throws Exception {
266    System.out.println(String.format("Verifying for auths %s, %s, %s, %s", CONFIDENTIAL, TOPSECRET,
267        SECRET, PRIVATE));
268    PrivilegedExceptionAction<Job> scanAction = new PrivilegedExceptionAction<Job>() {
269      @Override
270      public Job run() throws Exception {
271        return doVerify(conf, htd, CONFIDENTIAL, TOPSECRET, SECRET, PRIVATE);
272      }
273    };
274    Job job = USER1.runAs(scanAction);
275    this.numRowsReadWithExp1 = job.getCounters().findCounter(Counters.ROWS_VIS_EXP_1).getValue();
276    this.numRowsReadWithExp2 = job.getCounters().findCounter(Counters.ROWS_VIS_EXP_2).getValue();
277    this.numRowsReadWithExp3 = job.getCounters().findCounter(Counters.ROWS_VIS_EXP_3).getValue();
278    this.numRowsReadWithExp4 = job.getCounters().findCounter(Counters.ROWS_VIS_EXP_4).getValue();
279    assertEquals(this.numRowsLoadedWithExp1, this.numRowsReadWithExp1);
280    assertEquals(this.numRowsLoadedWithExp2, this.numRowsReadWithExp2);
281    assertEquals(0, this.numRowsReadWithExp3);
282    assertEquals(0, this.numRowsReadWithExp4);
283
284    // PUBLIC label auth is not provided for user1 user.
285    System.out.println(String.format("Verifying for auths %s, %s", PRIVATE, PUBLIC));
286    scanAction = new PrivilegedExceptionAction<Job>() {
287      @Override
288      public Job run() throws Exception {
289        return doVerify(conf, htd, PRIVATE, PUBLIC);
290      }
291    };
292    job = USER1.runAs(scanAction);
293    this.numRowsReadWithExp1 = job.getCounters().findCounter(Counters.ROWS_VIS_EXP_1).getValue();
294    this.numRowsReadWithExp2 = job.getCounters().findCounter(Counters.ROWS_VIS_EXP_2).getValue();
295    this.numRowsReadWithExp3 = job.getCounters().findCounter(Counters.ROWS_VIS_EXP_3).getValue();
296    this.numRowsReadWithExp4 = job.getCounters().findCounter(Counters.ROWS_VIS_EXP_4).getValue();
297    assertEquals(0, this.numRowsReadWithExp1);
298    assertEquals(0, this.numRowsReadWithExp2);
299    assertEquals(0, this.numRowsReadWithExp3);
300    assertEquals(this.numRowsLoadWithExp4, this.numRowsReadWithExp4);
301
302    // Normal user only having PUBLIC label auth and can view only those cells.
303    System.out.println(String.format("Verifying for auths %s, %s", PRIVATE, PUBLIC));
304    scanAction = new PrivilegedExceptionAction<Job>() {
305      @Override
306      public Job run() throws Exception {
307        return doVerify(conf, htd, PRIVATE, PUBLIC);
308      }
309    };
310    job = USER2.runAs(scanAction);
311    this.numRowsReadWithExp1 = job.getCounters().findCounter(Counters.ROWS_VIS_EXP_1).getValue();
312    this.numRowsReadWithExp2 = job.getCounters().findCounter(Counters.ROWS_VIS_EXP_2).getValue();
313    this.numRowsReadWithExp3 = job.getCounters().findCounter(Counters.ROWS_VIS_EXP_3).getValue();
314    this.numRowsReadWithExp4 = job.getCounters().findCounter(Counters.ROWS_VIS_EXP_4).getValue();
315    assertEquals(0, this.numRowsReadWithExp1);
316    assertEquals(0, this.numRowsReadWithExp2);
317    assertEquals(this.numRowsLoadWithExp3, this.numRowsReadWithExp3);
318    assertEquals(0, this.numRowsReadWithExp4);
319  }
320
321  private Job doVerify(Configuration conf, HTableDescriptor htd, String... auths)
322      throws IOException, InterruptedException, ClassNotFoundException {
323    Path outputDir = getTestDir(TEST_NAME, "verify-output");
324    Job job = new Job(conf);
325    job.setJarByClass(this.getClass());
326    job.setJobName(TEST_NAME + " Verification for " + htd.getTableName());
327    setJobScannerConf(job);
328    Scan scan = new Scan();
329    scan.setAuthorizations(new Authorizations(auths));
330    TableMapReduceUtil.initTableMapperJob(htd.getTableName().getNameAsString(), scan,
331        VerifyMapper.class, NullWritable.class, NullWritable.class, job);
332    TableMapReduceUtil.addDependencyJarsForClasses(job.getConfiguration(), AbstractHBaseTool.class);
333    int scannerCaching = conf.getInt("verify.scannercaching", SCANNER_CACHING);
334    TableMapReduceUtil.setScannerCaching(job, scannerCaching);
335    job.setNumReduceTasks(0);
336    FileOutputFormat.setOutputPath(job, outputDir);
337    assertTrue(job.waitForCompletion(true));
338    return job;
339  }
340
341  private static void setJobScannerConf(Job job) {
342    job.getConfiguration().setBoolean(ScannerCallable.LOG_SCANNER_ACTIVITY, true);
343    long lpr = job.getConfiguration().getLong(NUM_TO_WRITE_KEY, NUM_TO_WRITE_DEFAULT) / 100;
344    job.getConfiguration().setInt(TableRecordReaderImpl.LOG_PER_ROW_COUNT, (int) lpr);
345  }
346
347  @Override
348  public void printUsage() {
349    System.err.println(this.getClass().getSimpleName() + " -u usera,userb [-Doptions]");
350    System.err.println("  Loads a table with cell visibilities and verifies with Authorizations");
351    System.err.println("Options");
352    System.err
353        .println("  -Dloadmapper.table=<name>        Table to write/verify (default autogen)");
354    System.err.println("  -Dloadmapper.num_to_write=<n>    "
355        + "Number of rows per mapper (default 100,000 per mapper)");
356    System.err.println("  -Dloadmapper.numPresplits=<n>    "
357        + "Number of presplit regions to start with (default 40)");
358    System.err
359        .println("  -Dloadmapper.map.tasks=<n>       Number of map tasks for load (default 200)");
360    System.err.println("  -Dverify.scannercaching=<n>      "
361        + "Number hbase scanner caching rows to read (default 50)");
362  }
363
364  @Override
365  public int runTestFromCommandLine() throws Exception {
366    IntegrationTestingUtility.setUseDistributedCluster(getConf());
367    int numPresplits = getConf().getInt("loadmapper.numPresplits", 5);
368    // create HTableDescriptor for specified table
369    HTableDescriptor htd = new HTableDescriptor(getTablename());
370    htd.addFamily(new HColumnDescriptor(TEST_FAMILY));
371
372    try (Connection conn = ConnectionFactory.createConnection(getConf());
373        Admin admin = conn.getAdmin()) {
374      admin.createTable(htd, Bytes.toBytes(0L), Bytes.toBytes(-1L), numPresplits);
375    }
376    doLoad(getConf(), htd);
377    doVerify(getConf(), htd);
378    getTestingUtil(getConf()).deleteTable(getTablename());
379    return 0;
380  }
381
382  @SuppressWarnings("unchecked")
383  @Override
384  protected void processOptions(CommandLine cmd) {
385    List args = cmd.getArgList();
386    if (args.size() > 0) {
387      printUsage();
388      throw new RuntimeException("No args expected.");
389    }
390    // We always want loadAndVerify action
391    args.add("loadAndVerify");
392    if (cmd.hasOption(USER_OPT)) {
393      userNames = cmd.getOptionValue(USER_OPT);
394    }
395    super.processOptions(cmd);
396  }
397
398  public static void main(String argv[]) throws Exception {
399    Configuration conf = HBaseConfiguration.create();
400    IntegrationTestingUtility.setUseDistributedCluster(conf);
401    int ret = ToolRunner.run(conf, new IntegrationTestWithCellVisibilityLoadAndVerify(), argv);
402    System.exit(ret);
403  }
404}