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.coprocessor;
019
020import static org.junit.Assert.assertEquals;
021
022import com.google.protobuf.ServiceException;
023import java.io.File;
024import java.io.IOException;
025import java.security.PrivilegedExceptionAction;
026import java.util.Arrays;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.Map;
030import org.apache.hadoop.conf.Configuration;
031import org.apache.hadoop.fs.FileStatus;
032import org.apache.hadoop.fs.FileSystem;
033import org.apache.hadoop.fs.Path;
034import org.apache.hadoop.fs.permission.FsAction;
035import org.apache.hadoop.fs.permission.FsPermission;
036import org.apache.hadoop.hbase.HBaseClassTestRule;
037import org.apache.hadoop.hbase.HBaseTestingUtility;
038import org.apache.hadoop.hbase.TableName;
039import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
040import org.apache.hadoop.hbase.client.Connection;
041import org.apache.hadoop.hbase.client.ConnectionFactory;
042import org.apache.hadoop.hbase.client.Put;
043import org.apache.hadoop.hbase.client.Result;
044import org.apache.hadoop.hbase.client.ResultScanner;
045import org.apache.hadoop.hbase.client.Scan;
046import org.apache.hadoop.hbase.client.Table;
047import org.apache.hadoop.hbase.client.TableDescriptor;
048import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
049import org.apache.hadoop.hbase.mapreduce.ExportUtils;
050import org.apache.hadoop.hbase.mapreduce.Import;
051import org.apache.hadoop.hbase.protobuf.generated.VisibilityLabelsProtos;
052import org.apache.hadoop.hbase.security.HBaseKerberosUtils;
053import org.apache.hadoop.hbase.security.HadoopSecurityEnabledUserProviderForTesting;
054import org.apache.hadoop.hbase.security.User;
055import org.apache.hadoop.hbase.security.UserProvider;
056import org.apache.hadoop.hbase.security.access.AccessControlConstants;
057import org.apache.hadoop.hbase.security.access.AccessControlLists;
058import org.apache.hadoop.hbase.security.access.Permission;
059import org.apache.hadoop.hbase.security.access.SecureTestUtil;
060import org.apache.hadoop.hbase.security.access.SecureTestUtil.AccessTestAction;
061import org.apache.hadoop.hbase.security.visibility.Authorizations;
062import org.apache.hadoop.hbase.security.visibility.CellVisibility;
063import org.apache.hadoop.hbase.security.visibility.VisibilityClient;
064import org.apache.hadoop.hbase.security.visibility.VisibilityConstants;
065import org.apache.hadoop.hbase.security.visibility.VisibilityTestUtil;
066import org.apache.hadoop.hbase.testclassification.MediumTests;
067import org.apache.hadoop.hbase.util.Bytes;
068import org.apache.hadoop.hbase.util.Pair;
069import org.apache.hadoop.minikdc.MiniKdc;
070import org.apache.hadoop.security.UserGroupInformation;
071import org.apache.hadoop.util.ToolRunner;
072import org.junit.After;
073import org.junit.AfterClass;
074import org.junit.Before;
075import org.junit.BeforeClass;
076import org.junit.ClassRule;
077import org.junit.Rule;
078import org.junit.Test;
079import org.junit.experimental.categories.Category;
080import org.junit.rules.TestName;
081import org.slf4j.Logger;
082import org.slf4j.LoggerFactory;
083
084@Category({MediumTests.class})
085public class TestSecureExport {
086  @ClassRule
087  public static final HBaseClassTestRule CLASS_RULE =
088      HBaseClassTestRule.forClass(TestSecureExport.class);
089
090  private static final Logger LOG = LoggerFactory.getLogger(TestSecureExport.class);
091  private static final HBaseTestingUtility UTIL = new HBaseTestingUtility();
092  private static MiniKdc KDC;
093  private static final File KEYTAB_FILE = new File(UTIL.getDataTestDir("keytab").toUri().getPath());
094  private static String USERNAME;
095  private static String SERVER_PRINCIPAL;
096  private static String HTTP_PRINCIPAL;
097  private static final String FAMILYA_STRING = "fma";
098  private static final String FAMILYB_STRING = "fma";
099  private static final byte[] FAMILYA = Bytes.toBytes(FAMILYA_STRING);
100  private static final byte[] FAMILYB = Bytes.toBytes(FAMILYB_STRING);
101  private static final byte[] ROW1 = Bytes.toBytes("row1");
102  private static final byte[] ROW2 = Bytes.toBytes("row2");
103  private static final byte[] ROW3 = Bytes.toBytes("row3");
104  private static final byte[] QUAL = Bytes.toBytes("qual");
105  private static final String LOCALHOST = "localhost";
106  private static final long NOW = System.currentTimeMillis();
107  // user granted with all global permission
108  private static final String USER_ADMIN = "admin";
109  // user is table owner. will have all permissions on table
110  private static final String USER_OWNER = "owner";
111  // user with rx permissions.
112  private static final String USER_RX = "rxuser";
113  // user with exe-only permissions.
114  private static final String USER_XO = "xouser";
115  // user with read-only permissions.
116  private static final String USER_RO = "rouser";
117  // user with no permissions
118  private static final String USER_NONE = "noneuser";
119  private static final String PRIVATE = "private";
120  private static final String CONFIDENTIAL = "confidential";
121  private static final String SECRET = "secret";
122  private static final String TOPSECRET = "topsecret";
123  @Rule
124  public final TestName name = new TestName();
125  private static void setUpKdcServer() throws Exception {
126    KDC = UTIL.setupMiniKdc(KEYTAB_FILE);
127    USERNAME = UserGroupInformation.getLoginUser().getShortUserName();
128    SERVER_PRINCIPAL = USERNAME + "/" + LOCALHOST;
129    HTTP_PRINCIPAL = "HTTP/" + LOCALHOST;
130    KDC.createPrincipal(KEYTAB_FILE,
131      SERVER_PRINCIPAL,
132      HTTP_PRINCIPAL,
133      USER_ADMIN + "/" + LOCALHOST,
134      USER_OWNER + "/" + LOCALHOST,
135      USER_RX + "/" + LOCALHOST,
136      USER_RO + "/" + LOCALHOST,
137      USER_XO + "/" + LOCALHOST,
138      USER_NONE + "/" + LOCALHOST);
139  }
140
141  private static User getUserByLogin(final String user) throws IOException {
142    return User.create(UserGroupInformation.loginUserFromKeytabAndReturnUGI(
143        getPrinciple(user), KEYTAB_FILE.getAbsolutePath()));
144  }
145
146  private static String getPrinciple(final String user) {
147    return user + "/" + LOCALHOST + "@" + KDC.getRealm();
148  }
149
150  private static void setUpClusterKdc() throws Exception {
151    HBaseKerberosUtils.setSecuredConfiguration(UTIL.getConfiguration(),
152        SERVER_PRINCIPAL + "@" + KDC.getRealm(), HTTP_PRINCIPAL + "@" + KDC.getRealm());
153    HBaseKerberosUtils.setSSLConfiguration(UTIL, TestSecureExport.class);
154
155    UTIL.getConfiguration().set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY,
156        UTIL.getConfiguration().get(
157            CoprocessorHost.REGION_COPROCESSOR_CONF_KEY) + "," + Export.class.getName());
158  }
159
160  private static void addLabels(final Configuration conf, final List<String> users,
161      final List<String> labels) throws Exception {
162    PrivilegedExceptionAction<VisibilityLabelsProtos.VisibilityLabelsResponse> action
163      = () -> {
164        try (Connection conn = ConnectionFactory.createConnection(conf)) {
165          VisibilityClient.addLabels(conn, labels.toArray(new String[labels.size()]));
166          for (String user : users) {
167            VisibilityClient.setAuths(conn, labels.toArray(new String[labels.size()]), user);
168          }
169        } catch (Throwable t) {
170          throw new IOException(t);
171        }
172        return null;
173      };
174    getUserByLogin(USER_ADMIN).runAs(action);
175  }
176
177  @Before
178  public void announce() {
179    LOG.info("Running " + name.getMethodName());
180  }
181
182  @After
183  public void cleanup() throws IOException {
184  }
185
186  private static void clearOutput(Path path) throws IOException {
187    FileSystem fs = path.getFileSystem(UTIL.getConfiguration());
188    if (fs.exists(path)) {
189      assertEquals(true, fs.delete(path, true));
190    }
191  }
192
193  /**
194   * Sets the security firstly for getting the correct default realm.
195   */
196  @BeforeClass
197  public static void beforeClass() throws Exception {
198    UserProvider.setUserProviderForTesting(UTIL.getConfiguration(),
199        HadoopSecurityEnabledUserProviderForTesting.class);
200    setUpKdcServer();
201    SecureTestUtil.enableSecurity(UTIL.getConfiguration());
202    UTIL.getConfiguration().setBoolean(AccessControlConstants.EXEC_PERMISSION_CHECKS_KEY, true);
203    VisibilityTestUtil.enableVisiblityLabels(UTIL.getConfiguration());
204    SecureTestUtil.verifyConfiguration(UTIL.getConfiguration());
205    setUpClusterKdc();
206    UTIL.startMiniCluster();
207    UTIL.waitUntilAllRegionsAssigned(AccessControlLists.ACL_TABLE_NAME);
208    UTIL.waitUntilAllRegionsAssigned(VisibilityConstants.LABELS_TABLE_NAME);
209    UTIL.waitTableEnabled(AccessControlLists.ACL_TABLE_NAME, 50000);
210    UTIL.waitTableEnabled(VisibilityConstants.LABELS_TABLE_NAME, 50000);
211    SecureTestUtil.grantGlobal(UTIL, USER_ADMIN,
212            Permission.Action.ADMIN,
213            Permission.Action.CREATE,
214            Permission.Action.EXEC,
215            Permission.Action.READ,
216            Permission.Action.WRITE);
217    addLabels(UTIL.getConfiguration(), Arrays.asList(USER_OWNER),
218            Arrays.asList(PRIVATE, CONFIDENTIAL, SECRET, TOPSECRET));
219  }
220
221  @AfterClass
222  public static void afterClass() throws Exception {
223    if (KDC != null) {
224      KDC.stop();
225    }
226    UTIL.shutdownMiniCluster();
227  }
228
229  /**
230   * Test the ExportEndpoint's access levels. The {@link Export} test is ignored
231   * since the access exceptions cannot be collected from the mappers.
232   */
233  @Test
234  public void testAccessCase() throws Throwable {
235    final String exportTable = name.getMethodName();
236    TableDescriptor exportHtd = TableDescriptorBuilder
237            .newBuilder(TableName.valueOf(name.getMethodName()))
238            .setColumnFamily(ColumnFamilyDescriptorBuilder.of(FAMILYA))
239            .setOwnerString(USER_OWNER)
240            .build();
241    SecureTestUtil.createTable(UTIL, exportHtd, new byte[][]{Bytes.toBytes("s")});
242    SecureTestUtil.grantOnTable(UTIL, USER_RO,
243            TableName.valueOf(exportTable), null, null,
244            Permission.Action.READ);
245    SecureTestUtil.grantOnTable(UTIL, USER_RX,
246            TableName.valueOf(exportTable), null, null,
247            Permission.Action.READ,
248            Permission.Action.EXEC);
249    SecureTestUtil.grantOnTable(UTIL, USER_XO,
250            TableName.valueOf(exportTable), null, null,
251            Permission.Action.EXEC);
252    assertEquals(4, AccessControlLists.getTablePermissions(UTIL.getConfiguration(),
253            TableName.valueOf(exportTable)).size());
254    AccessTestAction putAction = () -> {
255      Put p = new Put(ROW1);
256      p.addColumn(FAMILYA, Bytes.toBytes("qual_0"), NOW, QUAL);
257      p.addColumn(FAMILYA, Bytes.toBytes("qual_1"), NOW, QUAL);
258      try (Connection conn = ConnectionFactory.createConnection(UTIL.getConfiguration());
259              Table t = conn.getTable(TableName.valueOf(exportTable))) {
260        t.put(p);
261      }
262      return null;
263    };
264    // no hdfs access.
265    SecureTestUtil.verifyAllowed(putAction,
266      getUserByLogin(USER_ADMIN),
267      getUserByLogin(USER_OWNER));
268    SecureTestUtil.verifyDenied(putAction,
269      getUserByLogin(USER_RO),
270      getUserByLogin(USER_XO),
271      getUserByLogin(USER_RX),
272      getUserByLogin(USER_NONE));
273
274    final FileSystem fs = UTIL.getDFSCluster().getFileSystem();
275    final Path openDir = fs.makeQualified(new Path("testAccessCase"));
276    fs.mkdirs(openDir);
277    fs.setPermission(openDir, new FsPermission(FsAction.ALL, FsAction.ALL, FsAction.ALL));
278    final Path output = fs.makeQualified(new Path(openDir, "output"));
279    AccessTestAction exportAction = () -> {
280      try {
281        String[] args = new String[]{exportTable, output.toString()};
282        Map<byte[], Export.Response> result
283                = Export.run(new Configuration(UTIL.getConfiguration()), args);
284        long rowCount = 0;
285        long cellCount = 0;
286        for (Export.Response r : result.values()) {
287          rowCount += r.getRowCount();
288          cellCount += r.getCellCount();
289        }
290        assertEquals(1, rowCount);
291        assertEquals(2, cellCount);
292        return null;
293      } catch (ServiceException | IOException ex) {
294        throw ex;
295      } catch (Throwable ex) {
296        LOG.error(ex.toString(), ex);
297        throw new Exception(ex);
298      } finally {
299        if (fs.exists(new Path(openDir, "output"))) {
300          // if export completes successfully, every file under the output directory should be
301          // owned by the current user, not the hbase service user.
302          FileStatus outputDirFileStatus = fs.getFileStatus(new Path(openDir, "output"));
303          String currentUserName = User.getCurrent().getShortName();
304          assertEquals("Unexpected file owner", currentUserName, outputDirFileStatus.getOwner());
305
306          FileStatus[] outputFileStatus = fs.listStatus(new Path(openDir, "output"));
307          for (FileStatus fileStatus: outputFileStatus) {
308            assertEquals("Unexpected file owner", currentUserName, fileStatus.getOwner());
309          }
310        } else {
311          LOG.info("output directory doesn't exist. Skip check");
312        }
313
314        clearOutput(output);
315      }
316    };
317    SecureTestUtil.verifyDenied(exportAction,
318      getUserByLogin(USER_RO),
319      getUserByLogin(USER_XO),
320      getUserByLogin(USER_NONE));
321    SecureTestUtil.verifyAllowed(exportAction,
322      getUserByLogin(USER_ADMIN),
323      getUserByLogin(USER_OWNER),
324      getUserByLogin(USER_RX));
325    AccessTestAction deleteAction = () -> {
326      UTIL.deleteTable(TableName.valueOf(exportTable));
327      return null;
328    };
329    SecureTestUtil.verifyAllowed(deleteAction, getUserByLogin(USER_OWNER));
330    fs.delete(openDir, true);
331  }
332
333  @Test
334  public void testVisibilityLabels() throws IOException, Throwable {
335    final String exportTable = name.getMethodName() + "_export";
336    final String importTable = name.getMethodName() + "_import";
337    final TableDescriptor exportHtd = TableDescriptorBuilder
338            .newBuilder(TableName.valueOf(exportTable))
339            .setColumnFamily(ColumnFamilyDescriptorBuilder.of(FAMILYA))
340            .setOwnerString(USER_OWNER)
341            .build();
342    SecureTestUtil.createTable(UTIL, exportHtd, new byte[][]{Bytes.toBytes("s")});
343    AccessTestAction putAction = () -> {
344      Put p1 = new Put(ROW1);
345      p1.addColumn(FAMILYA, QUAL, NOW, QUAL);
346      p1.setCellVisibility(new CellVisibility(SECRET));
347      Put p2 = new Put(ROW2);
348      p2.addColumn(FAMILYA, QUAL, NOW, QUAL);
349      p2.setCellVisibility(new CellVisibility(PRIVATE + " & " + CONFIDENTIAL));
350      Put p3 = new Put(ROW3);
351      p3.addColumn(FAMILYA, QUAL, NOW, QUAL);
352      p3.setCellVisibility(new CellVisibility("!" + CONFIDENTIAL + " & " + TOPSECRET));
353      try (Connection conn = ConnectionFactory.createConnection(UTIL.getConfiguration());
354              Table t = conn.getTable(TableName.valueOf(exportTable))) {
355        t.put(p1);
356        t.put(p2);
357        t.put(p3);
358      }
359      return null;
360    };
361    SecureTestUtil.verifyAllowed(putAction, getUserByLogin(USER_OWNER));
362    List<Pair<List<String>, Integer>> labelsAndRowCounts = new LinkedList<>();
363    labelsAndRowCounts.add(new Pair<>(Arrays.asList(SECRET), 1));
364    labelsAndRowCounts.add(new Pair<>(Arrays.asList(PRIVATE, CONFIDENTIAL), 1));
365    labelsAndRowCounts.add(new Pair<>(Arrays.asList(TOPSECRET), 1));
366    labelsAndRowCounts.add(new Pair<>(Arrays.asList(TOPSECRET, CONFIDENTIAL), 0));
367    labelsAndRowCounts.add(new Pair<>(Arrays.asList(TOPSECRET, CONFIDENTIAL, PRIVATE, SECRET), 2));
368    for (final Pair<List<String>, Integer> labelsAndRowCount : labelsAndRowCounts) {
369      final List<String> labels = labelsAndRowCount.getFirst();
370      final int rowCount = labelsAndRowCount.getSecond();
371      //create a open permission directory.
372      final Path openDir = new Path("testAccessCase");
373      final FileSystem fs = openDir.getFileSystem(UTIL.getConfiguration());
374      fs.mkdirs(openDir);
375      fs.setPermission(openDir, new FsPermission(FsAction.ALL, FsAction.ALL, FsAction.ALL));
376      final Path output = fs.makeQualified(new Path(openDir, "output"));
377      AccessTestAction exportAction = () -> {
378        StringBuilder buf = new StringBuilder();
379        labels.forEach(v -> buf.append(v).append(","));
380        buf.deleteCharAt(buf.length() - 1);
381        try {
382          String[] args = new String[]{
383            "-D " + ExportUtils.EXPORT_VISIBILITY_LABELS + "=" + buf.toString(),
384            exportTable,
385            output.toString(),};
386          Export.run(new Configuration(UTIL.getConfiguration()), args);
387          return null;
388        } catch (ServiceException | IOException ex) {
389          throw ex;
390        } catch (Throwable ex) {
391          throw new Exception(ex);
392        }
393      };
394      SecureTestUtil.verifyAllowed(exportAction, getUserByLogin(USER_OWNER));
395      final TableDescriptor importHtd = TableDescriptorBuilder
396              .newBuilder(TableName.valueOf(importTable))
397              .setColumnFamily(ColumnFamilyDescriptorBuilder.of(FAMILYB))
398              .setOwnerString(USER_OWNER)
399              .build();
400      SecureTestUtil.createTable(UTIL, importHtd, new byte[][]{Bytes.toBytes("s")});
401      AccessTestAction importAction = () -> {
402        String[] args = new String[]{
403          "-D" + Import.CF_RENAME_PROP + "=" + FAMILYA_STRING + ":" + FAMILYB_STRING,
404          importTable,
405          output.toString()
406        };
407        assertEquals(0, ToolRunner.run(
408            new Configuration(UTIL.getConfiguration()), new Import(), args));
409        return null;
410      };
411      SecureTestUtil.verifyAllowed(importAction, getUserByLogin(USER_OWNER));
412      AccessTestAction scanAction = () -> {
413        Scan scan = new Scan();
414        scan.setAuthorizations(new Authorizations(labels));
415        try (Connection conn = ConnectionFactory.createConnection(UTIL.getConfiguration());
416                Table table = conn.getTable(importHtd.getTableName());
417                ResultScanner scanner = table.getScanner(scan)) {
418          int count = 0;
419          for (Result r : scanner) {
420            ++count;
421          }
422          assertEquals(rowCount, count);
423        }
424        return null;
425      };
426      SecureTestUtil.verifyAllowed(scanAction, getUserByLogin(USER_OWNER));
427      AccessTestAction deleteAction = () -> {
428        UTIL.deleteTable(importHtd.getTableName());
429        return null;
430      };
431      SecureTestUtil.verifyAllowed(deleteAction, getUserByLogin(USER_OWNER));
432      clearOutput(output);
433    }
434    AccessTestAction deleteAction = () -> {
435      UTIL.deleteTable(exportHtd.getTableName());
436      return null;
437    };
438    SecureTestUtil.verifyAllowed(deleteAction, getUserByLogin(USER_OWNER));
439  }
440}