View Javadoc

1   /**
2    * Copyright 2011 The Apache Software Foundation
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS,
16   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17   * See the License for the specific language governing permissions and
18   * limitations under the License.
19   */
20  package org.apache.hadoop.hbase.security.access;
21  
22  import org.apache.commons.logging.Log;
23  import org.apache.commons.logging.LogFactory;
24  import org.apache.hadoop.classification.InterfaceAudience;
25  import org.apache.hadoop.conf.Configuration;
26  import org.apache.hadoop.fs.FileStatus;
27  import org.apache.hadoop.fs.FileSystem;
28  import org.apache.hadoop.fs.Path;
29  import org.apache.hadoop.fs.permission.FsPermission;
30  import org.apache.hadoop.hbase.CoprocessorEnvironment;
31  import org.apache.hadoop.hbase.DoNotRetryIOException;
32  import org.apache.hadoop.hbase.coprocessor.BaseEndpointCoprocessor;
33  import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
34  import org.apache.hadoop.hbase.ipc.RequestContext;
35  import org.apache.hadoop.hbase.regionserver.HRegion;
36  import org.apache.hadoop.hbase.security.User;
37  import org.apache.hadoop.hbase.util.Bytes;
38  import org.apache.hadoop.hbase.util.Methods;
39  import org.apache.hadoop.hbase.util.Pair;
40  import org.apache.hadoop.security.UserGroupInformation;
41  import org.apache.hadoop.security.token.Token;
42  
43  import java.io.IOException;
44  import java.math.BigInteger;
45  import java.security.PrivilegedAction;
46  import java.security.SecureRandom;
47  import java.util.List;
48  
49  /**
50   * Coprocessor service for bulk loads in secure mode.
51   * This coprocessor has to be installed as part of enabling
52   * security in HBase.
53   *
54   * This service addresses two issues:
55   *
56   * 1. Moving files in a secure filesystem wherein the HBase Client
57   * and HBase Server are different filesystem users.
58   * 2. Does moving in a secure manner. Assuming that the filesystem
59   * is POSIX compliant.
60   *
61   * The algorithm is as follows:
62   *
63   * 1. Create an hbase owned staging directory which is
64   * world traversable (711): /hbase/staging
65   * 2. A user writes out data to his secure output directory: /user/foo/data
66   * 3. A call is made to hbase to create a secret staging directory
67   * which globally rwx (777): /user/staging/averylongandrandomdirectoryname
68   * 4. The user makes the data world readable and writable, then moves it
69   * into the random staging directory, then calls bulkLoadHFiles()
70   *
71   * Like delegation tokens the strength of the security lies in the length
72   * and randomness of the secret directory.
73   *
74   */
75  @InterfaceAudience.Private
76  public class SecureBulkLoadEndpoint extends BaseEndpointCoprocessor
77      implements SecureBulkLoadProtocol {
78  
79    public static final long VERSION = 0L;
80  
81    //Random number is 320 bits wide
82    private static final int RANDOM_WIDTH = 320;
83    //We picked 32 as the radix, so the character set
84    //will only contain alpha numeric values
85    //320/5 = 64 characters
86    private static final int RANDOM_RADIX = 32;
87  
88    private static Log LOG = LogFactory.getLog(SecureBulkLoadEndpoint.class);
89  
90    private final static FsPermission PERM_ALL_ACCESS = FsPermission.valueOf("-rwxrwxrwx");
91    private final static FsPermission PERM_HIDDEN = FsPermission.valueOf("-rwx--x--x");
92    private final static String BULKLOAD_STAGING_DIR = "hbase.bulkload.staging.dir";
93  
94    private SecureRandom random;
95    private FileSystem fs;
96    private Configuration conf;
97  
98    //two levels so it doesn't get deleted accidentally
99    //no sticky bit in Hadoop 1.0
100   private Path baseStagingDir;
101 
102   private RegionCoprocessorEnvironment env;
103 
104 
105   @Override
106   public void start(CoprocessorEnvironment env) {
107     super.start(env);
108 
109     this.env = (RegionCoprocessorEnvironment)env;
110     random = new SecureRandom();
111     conf = env.getConfiguration();
112     baseStagingDir = getBaseStagingDir(conf);
113 
114     try {
115       fs = FileSystem.get(conf);
116       fs.mkdirs(baseStagingDir, PERM_HIDDEN);
117       fs.setPermission(baseStagingDir, PERM_HIDDEN);
118       //no sticky bit in hadoop-1.0, making directory nonempty so it never gets erased
119       fs.mkdirs(new Path(baseStagingDir,"DONOTERASE"), PERM_HIDDEN);
120       FileStatus status = fs.getFileStatus(baseStagingDir);
121       if(status == null) {
122         throw new IllegalStateException("Failed to create staging directory");
123       }
124       if(!status.getPermission().equals(PERM_HIDDEN)) {
125         throw new IllegalStateException("Directory already exists but permissions aren't set to '-rwx--x--x' ");
126       }
127     } catch (IOException e) {
128       throw new IllegalStateException("Failed to get FileSystem instance",e);
129     }
130   }
131 
132   @Override
133   public String prepareBulkLoad(byte[] tableName) throws IOException {
134     getAccessController().prePrepareBulkLoad(env);
135     return createStagingDir(baseStagingDir, getActiveUser(), tableName).toString();
136   }
137 
138   @Override
139   public void cleanupBulkLoad(String bulkToken) throws IOException {
140     getAccessController().preCleanupBulkLoad(env);
141     fs.delete(createStagingDir(baseStagingDir,
142         getActiveUser(),
143         env.getRegion().getTableDesc().getName(),
144         new Path(bulkToken).getName()),
145         true);
146   }
147 
148   @Override
149   public boolean bulkLoadHFiles(final List<Pair<byte[], String>> familyPaths,
150                                 final Token<?> userToken, final String bulkToken) throws IOException {
151     User user = getActiveUser();
152     final UserGroupInformation ugi = user.getUGI();
153     if(userToken != null) {
154       ugi.addToken(userToken);
155     } else if(User.isSecurityEnabled()) {
156       //we allow this to pass through in "simple" security mode
157       //for mini cluster testing
158       throw new DoNotRetryIOException("User token cannot be null");
159     }
160 
161     HRegion region = env.getRegion();
162     boolean bypass = false;
163     if (region.getCoprocessorHost() != null) {
164       bypass = region.getCoprocessorHost().preBulkLoadHFile(familyPaths);
165     }
166     boolean loaded = false;
167     if (!bypass) {
168       loaded = ugi.doAs(new PrivilegedAction<Boolean>() {
169         @Override
170         public Boolean run() {
171           FileSystem fs = null;
172           try {
173             Configuration conf = env.getConfiguration();
174             fs = FileSystem.get(conf);
175             for(Pair<byte[], String> el: familyPaths) {
176               Path p = new Path(el.getSecond());
177               LOG.debug("Setting permission for: " + p);
178               fs.setPermission(p, PERM_ALL_ACCESS);
179               Path stageFamily = new Path(bulkToken, Bytes.toString(el.getFirst()));
180               if(!fs.exists(stageFamily)) {
181                 fs.mkdirs(stageFamily);
182                 fs.setPermission(stageFamily, PERM_ALL_ACCESS);
183               }
184             }
185             //We call bulkLoadHFiles as requesting user
186             //To enable access prior to staging
187             return env.getRegion().bulkLoadHFiles(familyPaths,
188                 new SecureBulkLoadListener(fs, bulkToken));
189           } catch (Exception e) {
190             LOG.error("Failed to complete bulk load", e);
191           }
192           return false;
193         }
194       });
195     }
196     if (region.getCoprocessorHost() != null) {
197       loaded = region.getCoprocessorHost().postBulkLoadHFile(familyPaths, loaded);
198     }
199     return loaded;
200   }
201 
202   @Override
203   public long getProtocolVersion(String protocol, long clientVersion)
204       throws IOException {
205     if (SecureBulkLoadProtocol.class.getName().equals(protocol)) {
206       return SecureBulkLoadEndpoint.VERSION;
207     }
208     LOG.warn("Unknown protocol requested: " + protocol);
209     return -1;
210   }
211 
212   private AccessController getAccessController() {
213     return (AccessController) this.env.getRegion()
214         .getCoprocessorHost().findCoprocessor(AccessController.class.getName());
215   }
216 
217   private Path createStagingDir(Path baseDir, User user, byte[] tableName) throws IOException {
218     String randomDir = user.getShortName()+"__"+Bytes.toString(tableName)+"__"+
219         (new BigInteger(RANDOM_WIDTH, random).toString(RANDOM_RADIX));
220     return createStagingDir(baseDir, user, tableName, randomDir);
221   }
222 
223   private Path createStagingDir(Path baseDir,
224                                 User user,
225                                 byte[] tableName,
226                                 String randomDir) throws IOException {
227     Path p = new Path(baseDir, randomDir);
228     fs.mkdirs(p, PERM_ALL_ACCESS);
229     fs.setPermission(p, PERM_ALL_ACCESS);
230     return p;
231   }
232 
233   private User getActiveUser() throws IOException {
234     User user = RequestContext.getRequestUser();
235     if (!RequestContext.isInRequestContext()) {
236       throw new DoNotRetryIOException("Failed to get requesting user");
237     }
238 
239     //this is for testing
240     if("simple".equalsIgnoreCase(conf.get(User.HBASE_SECURITY_CONF_KEY))) {
241       return User.createUserForTesting(conf, user.getShortName(), new String[]{});
242     }
243 
244     return user;
245   }
246 
247   /**
248    * This returns the staging path for a given column family.
249    * This is needed for clean recovery and called reflectively in LoadIncrementalHFiles
250    */
251   public static Path getStagingPath(Configuration conf, String bulkToken, byte[] family) {
252     Path stageP = new Path(getBaseStagingDir(conf), bulkToken);
253     return new Path(stageP, Bytes.toString(family));
254   }
255 
256   private static Path getBaseStagingDir(Configuration conf) {
257     return new Path(conf.get(BULKLOAD_STAGING_DIR, "/tmp/hbase-staging"));
258   }
259 
260   private static class SecureBulkLoadListener implements HRegion.BulkLoadListener {
261     private FileSystem fs;
262     private String stagingDir;
263 
264     public SecureBulkLoadListener(FileSystem fs, String stagingDir) {
265       this.fs = fs;
266       this.stagingDir = stagingDir;
267     }
268 
269     @Override
270     public String prepareBulkLoad(final byte[] family, final String srcPath) throws IOException {
271       Path p = new Path(srcPath);
272       Path stageP = new Path(stagingDir, new Path(Bytes.toString(family), p.getName()));
273 
274       if(!isFile(p)) {
275         throw new IOException("Path does not reference a file: " + p);
276       }
277 
278       LOG.debug("Moving " + p + " to " + stageP);
279       if(!fs.rename(p, stageP)) {
280         throw new IOException("Failed to move HFile: " + p + " to " + stageP);
281       }
282       return stageP.toString();
283     }
284 
285     @Override
286     public void doneBulkLoad(byte[] family, String srcPath) throws IOException {
287       LOG.debug("Bulk Load done for: " + srcPath);
288     }
289 
290     @Override
291     public void failedBulkLoad(final byte[] family, final String srcPath) throws IOException {
292       Path p = new Path(srcPath);
293       Path stageP = new Path(stagingDir,
294           new Path(Bytes.toString(family), p.getName()));
295       LOG.debug("Moving " + stageP + " back to " + p);
296       if(!fs.rename(stageP, p))
297         throw new IOException("Failed to move HFile: " + stageP + " to " + p);
298     }
299 
300     /**
301      * Check if the path is referencing a file.
302      * This is mainly needed to avoid symlinks.
303      * @param p
304      * @return true if the p is a file
305      * @throws IOException
306      */
307     private boolean isFile(Path p) throws IOException {
308       FileStatus status = fs.getFileStatus(p);
309       boolean isFile = !status.isDir();
310       try {
311         isFile = isFile && !(Boolean)Methods.call(FileStatus.class, status, "isSymlink", null, null);
312       } catch (Exception e) {
313       }
314       return isFile;
315     }
316   }
317 }