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.fs;
019
020import java.io.IOException;
021import java.util.LinkedList;
022import java.util.regex.Matcher;
023import java.util.regex.Pattern;
024
025import org.apache.commons.logging.Log;
026import org.apache.hadoop.classification.InterfaceAudience;
027import org.apache.hadoop.classification.InterfaceStability;
028import org.apache.hadoop.fs.permission.ChmodParser;
029import org.apache.hadoop.fs.permission.FsPermission;
030import org.apache.hadoop.fs.shell.CommandFactory;
031import org.apache.hadoop.fs.shell.CommandFormat;
032import org.apache.hadoop.fs.shell.FsCommand;
033import org.apache.hadoop.fs.shell.PathData;
034import 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
042public 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. " +
067      "This works similar to the shell's chmod command with a few exceptions.\n" +
068      "-R: modifies the files recursively. This is the only option" +
069      " currently supported.\n" +
070      "<MODE>: Mode is the same as mode used for the shell's command. " +
071      "The only letters recognized are 'rwxXt', e.g. +t,a+r,g-w,+rwx,o=r.\n" +
072      "<OCTALMODE>: Mode specifed in 3 or 4 digits. If 4 digits, the first " +
073      "may be 1 or 0 to turn the sticky bit on or off, respectively.  Unlike " +
074      "the shell command, it is not possible to specify only part of the " +
075      "mode, e.g. 754 is same as u=rwx,g=rx,o=r.\n\n" +
076      "If none of 'augo' is specified, 'a' is assumed and unlike the " +
077      "shell 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. " +
125      "This is similar to the shell's chown command with a few exceptions.\n" +
126      "-R: modifies the files recursively. This is the only option " +
127      "currently supported.\n\n" +
128      "If only the owner or group is specified, then only the owner or " +
129      "group is modified. " +
130      "The owner and group names may only consist of digits, alphabet, "+
131      "and any of " + allowedChars + ". The names are case sensitive.\n\n" +
132      "WARNING: Avoid using '.' to separate user name and group though " +
133      "Linux allows it. If user names have dots in them and you are " +
134      "using local file system, you might see surprising results since " +
135      "the shell 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}