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}