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     */
018    package org.apache.hadoop.fs;
019    
020    import java.io.IOException;
021    import java.util.LinkedList;
022    import java.util.regex.Matcher;
023    import java.util.regex.Pattern;
024    
025    import org.apache.commons.logging.Log;
026    import org.apache.hadoop.classification.InterfaceAudience;
027    import org.apache.hadoop.classification.InterfaceStability;
028    import org.apache.hadoop.fs.permission.ChmodParser;
029    import org.apache.hadoop.fs.permission.FsPermission;
030    import org.apache.hadoop.fs.shell.CommandFactory;
031    import org.apache.hadoop.fs.shell.CommandFormat;
032    import org.apache.hadoop.fs.shell.FsCommand;
033    import org.apache.hadoop.fs.shell.PathData;
034    import org.apache.hadoop.util.Shell;
035    
036    /**
037     * This class is the home for file permissions related commands.
038     * Moved to this separate class since FsShell is getting too large.
039     */
040    @InterfaceAudience.Private
041    @InterfaceStability.Unstable
042    public class FsShellPermissions extends FsCommand {
043    
044      static Log LOG = FsShell.LOG;
045      
046      /**
047       * Register the permission related commands with the factory
048       * @param factory the command factory
049       */
050      public static void registerCommands(CommandFactory factory) {
051        factory.addClass(Chmod.class, "-chmod");
052        factory.addClass(Chown.class, "-chown");
053        factory.addClass(Chgrp.class, "-chgrp");
054      }
055    
056      /**
057       * The pattern is almost as flexible as mode allowed by chmod shell command.
058       * The main restriction is that we recognize only rwxXt. To reduce errors we
059       * also enforce octal mode specifications of either 3 digits without a sticky
060       * bit setting or four digits with a sticky bit setting.
061       */
062      public static class Chmod extends FsShellPermissions {
063        public static final String NAME = "chmod";
064        public static final String USAGE = "[-R] <MODE[,MODE]... | OCTALMODE> PATH...";
065        public static final String DESCRIPTION =
066          "Changes permissions of a file.\n" +
067          "\tThis works similar to shell's chmod with a few exceptions.\n\n" +
068          "-R\tmodifies the files recursively. This is the only option\n" +
069          "\tcurrently supported.\n\n" +
070          "MODE\tMode is same as mode used for chmod shell command.\n" +
071          "\tOnly letters recognized are 'rwxXt'. E.g. +t,a+r,g-w,+rwx,o=r\n\n" +
072          "OCTALMODE Mode specifed in 3 or 4 digits. If 4 digits, the first may\n" +
073          "be 1 or 0 to turn the sticky bit on or off, respectively.  Unlike " +
074          "shell command, it is not possible to specify only part of the mode\n" +
075          "\tE.g. 754 is same as u=rwx,g=rx,o=r\n\n" +
076          "\tIf none of 'augo' is specified, 'a' is assumed and unlike\n" +
077          "\tshell command, no umask is applied.";
078    
079        protected ChmodParser pp;
080    
081        @Override
082        protected void processOptions(LinkedList<String> args) throws IOException {
083          CommandFormat cf = new CommandFormat(2, Integer.MAX_VALUE, "R", null);
084          cf.parse(args);
085          setRecursive(cf.getOpt("R"));
086    
087          String modeStr = args.removeFirst();
088          try {
089            pp = new ChmodParser(modeStr);
090          } catch (IllegalArgumentException iea) {
091            // TODO: remove "chmod : " so it's not doubled up in output, but it's
092            // here for backwards compatibility...
093            throw new IllegalArgumentException(
094                "chmod : mode '" + modeStr + "' does not match the expected pattern.");      
095          }
096        }
097        
098        @Override
099        protected void processPath(PathData item) throws IOException {
100          short newperms = pp.applyNewPermission(item.stat);
101          if (item.stat.getPermission().toShort() != newperms) {
102            try {
103              item.fs.setPermission(item.path, new FsPermission(newperms));
104            } catch (IOException e) {
105              LOG.debug("Error changing permissions of " + item, e);
106              throw new IOException(
107                  "changing permissions of '" + item + "': " + e.getMessage());
108            }
109          }
110        }    
111      }
112      
113      // used by chown/chgrp
114      static private String allowedChars = Shell.WINDOWS ? "[-_./@a-zA-Z0-9 ]" :
115        "[-_./@a-zA-Z0-9]";
116    
117      /**
118       * Used to change owner and/or group of files 
119       */
120      public static class Chown extends FsShellPermissions {
121        public static final String NAME = "chown";
122        public static final String USAGE = "[-R] [OWNER][:[GROUP]] PATH...";
123        public static final String DESCRIPTION =
124          "Changes owner and group of a file.\n" +
125          "\tThis is similar to shell's chown with a few exceptions.\n\n" +
126          "\t-R\tmodifies the files recursively. This is the only option\n" +
127          "\tcurrently supported.\n\n" +
128          "\tIf only owner or group is specified then only owner or\n" +
129          "\tgroup is modified.\n\n" +
130          "\tThe owner and group names may only consist of digits, alphabet,\n"+
131          "\tand any of " + allowedChars + ". The names are case sensitive.\n\n" +
132          "\tWARNING: Avoid using '.' to separate user name and group though\n" +
133          "\tLinux allows it. If user names have dots in them and you are\n" +
134          "\tusing local file system, you might see surprising results since\n" +
135          "\tshell command 'chown' is used for local files.";
136    
137        ///allows only "allowedChars" above in names for owner and group
138        static private final Pattern chownPattern = Pattern.compile(
139            "^\\s*(" + allowedChars + "+)?([:](" + allowedChars + "*))?\\s*$");
140    
141        protected String owner = null;
142        protected String group = null;
143    
144        @Override
145        protected void processOptions(LinkedList<String> args) throws IOException {
146          CommandFormat cf = new CommandFormat(2, Integer.MAX_VALUE, "R");
147          cf.parse(args);
148          setRecursive(cf.getOpt("R"));
149          parseOwnerGroup(args.removeFirst());
150        }
151        
152        /**
153         * Parse the first argument into an owner and group
154         * @param ownerStr string describing new ownership
155         */
156        protected void parseOwnerGroup(String ownerStr) {
157          Matcher matcher = chownPattern.matcher(ownerStr);
158          if (!matcher.matches()) {
159            throw new IllegalArgumentException(
160                "'" + ownerStr + "' does not match expected pattern for [owner][:group].");
161          }
162          owner = matcher.group(1);
163          group = matcher.group(3);
164          if (group != null && group.length() == 0) {
165            group = null;
166          }
167          if (owner == null && group == null) {
168            throw new IllegalArgumentException(
169                "'" + ownerStr + "' does not specify owner or group.");
170          }    
171        }
172        
173        @Override
174        protected void processPath(PathData item) throws IOException {
175          //Should we do case insensitive match?
176          String newOwner = (owner == null || owner.equals(item.stat.getOwner())) ?
177                            null : owner;
178          String newGroup = (group == null || group.equals(item.stat.getGroup())) ?
179                            null : group;
180    
181          if (newOwner != null || newGroup != null) {
182            try {
183              item.fs.setOwner(item.path, newOwner, newGroup);
184            } catch (IOException e) {
185              LOG.debug("Error changing ownership of " + item, e);
186              throw new IOException(
187                  "changing ownership of '" + item + "': " + e.getMessage());
188            }
189          }
190        }
191      }
192    
193      /**
194       * Used to change group of files 
195       */
196      public static class Chgrp extends Chown {
197        public static final String NAME = "chgrp";
198        public static final String USAGE = "[-R] GROUP PATH...";
199        public static final String DESCRIPTION =
200          "This is equivalent to -chown ... :GROUP ...";
201    
202        static private final Pattern chgrpPattern = 
203          Pattern.compile("^\\s*(" + allowedChars + "+)\\s*$");
204    
205        @Override
206        protected void parseOwnerGroup(String groupStr) {
207          Matcher matcher = chgrpPattern.matcher(groupStr);
208          if (!matcher.matches()) {
209            throw new IllegalArgumentException(
210                "'" + groupStr + "' does not match expected pattern for group");
211          }
212          owner = null;
213          group = matcher.group(1);
214        }
215      }
216    }