summaryrefslogtreecommitdiffstats
path: root/tools/checkperms/checkperms.py
blob: a396c55a1e9233ad93bd6bbc925b5880fda145f5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
#!/usr/bin/python
# Copyright (c) 2010 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Makes sure files have the right permissions.

Some developers have broken SCM configurations that flip the svn:executable
permission on for no good reason. Unix developers who run ls --color will then
see .cc files in green and get confused.

To ignore a particular file, add it to WHITELIST_FILES.
To ignore a particular extension, add it to WHITELIST_EXTENSIONS.
To ignore whatever regexps your heart desires, add it WHITELIST_REGEX.

Note that all directory separators must be slashes (Unix-style) and not
backslashes. All directories should be relative to the source root and all
file paths should be only lowercase.
"""

import optparse
import os
import pipes
import re
import stat
import sys

#### USER EDITABLE SECTION STARTS HERE ####

# Files with these extensions are allowed to have executable permissions.
WHITELIST_EXTENSIONS = [
    'bash',
    'bat',
    'dylib',
    'pl',
    'py',
    'rb',
    'sh',
]

# Files that end the following paths are whitelisted too.
WHITELIST_FILES = [
    '/build/gyp_chromium',
    '/build/linux/dump_app_syms',
    '/build/linux/pkg-config-wrapper',
    '/build/mac/strip_from_xcode',
    '/build/mac/strip_save_dsym',
    '/chrome/installer/mac/pkg-dmg',
    '/chrome/tools/build/linux/chrome-wrapper',
    '/chrome/tools/build/mac/build_app_dmg',
    '/chrome/tools/build/mac/clean_up_old_versions',
    '/chrome/tools/build/mac/copy_framework_unversioned',
    '/chrome/tools/build/mac/dump_product_syms',
    '/chrome/tools/build/mac/generate_localizer',
    '/chrome/tools/build/mac/make_sign_sh',
    '/chrome/tools/build/mac/tweak_info_plist',
    '/chrome/tools/build/mac/verify_order',
    '/o3d/build/gyp_o3d',
    '/o3d/gypbuild',
    '/o3d/installer/linux/debian.in/rules',
    '/third_party/icu/source/runconfigureicu',
    '/third_party/lcov/bin/gendesc',
    '/third_party/lcov/bin/genhtml',
    '/third_party/lcov/bin/geninfo',
    '/third_party/lcov/bin/genpng',
    '/third_party/lcov/bin/lcov',
    '/third_party/lcov/bin/mcov',
    '/third_party/libxml/linux/xml2-config',
    '/third_party/lzma_sdk/executable/7za.exe',
    '/third_party/swig/linux/swig',
    '/third_party/tcmalloc/chromium/src/pprof',
    '/tools/git/post-checkout',
    '/tools/git/post-merge',
]

# File names that are always whitelisted.  (These are all autoconf spew.)
WHITELIST_FILENAMES = set((
  'config.guess',
  'config.sub',
  'configure',
  'depcomp',
  'install-sh',
  'missing',
  'mkinstalldirs'
))

# File paths that contain these regexps will be whitelisted as well.
WHITELIST_REGEX = [
    re.compile('/third_party/sqlite/'),
    re.compile('/third_party/xdg-utils/'),
    re.compile('/third_party/yasm/source/patched-yasm/config'),
]

#### USER EDITABLE SECTION ENDS HERE ####

WHITELIST_EXTENSIONS_REGEX = re.compile(r'\.(%s)' %
                                        '|'.join(WHITELIST_EXTENSIONS))

WHITELIST_FILES_REGEX = re.compile(r'(%s)$' % '|'.join(WHITELIST_FILES))

# Set to true for more output. This is set by the command line options.
VERBOSE = False

# In lowercase, using forward slashes as directory separators, ending in a
# forward slash. Set by the command line options.
BASE_DIRECTORY = ''

# The default if BASE_DIRECTORY is not set on the command line.
DEFAULT_BASE_DIRECTORY = '../../..'

# The directories which contain the sources managed by git.
GIT_SOURCE_DIRECTORY = set()

# The SVN repository url.
SVN_REPO_URL = ''

# Whether we are using SVN or GIT.
IS_SVN = True

# Executable permission mask
EXECUTABLE_PERMISSION = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH


def IsWhiteListed(file_path):
  """Returns True if file_path is in our whitelist of files to ignore."""
  if WHITELIST_EXTENSIONS_REGEX.match(os.path.splitext(file_path)[1]):
    return True
  if WHITELIST_FILES_REGEX.search(file_path):
    return True
  if os.path.basename(file_path) in WHITELIST_FILENAMES:
    return True
  for regex in WHITELIST_REGEX:
    if regex.search(file_path):
      return True
  return False


def CheckFile(file_path):
  """Checks file_path's permissions.

  Args:
    file_path: The file path to check.

  Returns:
    Either a string describing the error if there was one, or None if the file
    checked out OK.
  """
  if VERBOSE:
    print 'Checking file: ' + file_path

  file_path_lower = file_path.lower()
  if IsWhiteListed(file_path_lower):
    return None

  # Not whitelisted, stat the file and check permissions.
  try:
    st_mode = os.stat(file_path).st_mode
  except IOError, e:
    return 'Failed to stat file: %s' % e
  except OSError, e:
    return 'Failed to stat file: %s' % e

  if EXECUTABLE_PERMISSION & st_mode:
    error = 'Contains executable permission'
    if VERBOSE:
      return '%s: %06o' % (error, st_mode)
    return error
  return None


def ShouldCheckDirectory(dir_path):
  """Determine if we should check the content of dir_path."""
  if not IS_SVN:
    return dir_path in GIT_SOURCE_DIRECTORY
  repo_url = GetSvnRepositoryRoot(dir_path)
  if not repo_url:
    return False
  return repo_url == SVN_REPO_URL


def CheckDirectory(dir_path):
  """Check the files in dir_path; recursively check its subdirectories."""
  # Collect a list of all files and directories to check.
  files_to_check = []
  dirs_to_check = []
  success = True
  contents = os.listdir(dir_path)
  for cur in contents:
    full_path = os.path.join(dir_path, cur)
    if os.path.isdir(full_path) and ShouldCheckDirectory(full_path):
      dirs_to_check.append(full_path)
    elif os.path.isfile(full_path):
      files_to_check.append(full_path)

  # First check all files in this directory.
  for cur_file in files_to_check:
    file_status = CheckFile(cur_file)
    if file_status is not None:
      print 'ERROR in %s\n%s' % (cur_file, file_status)
      success = False

  # Next recurse into the subdirectories.
  for cur_dir in dirs_to_check:
    if not CheckDirectory(cur_dir):
      success = False
  return success


def GetGitSourceDirectory(root):
  """Returns a set of the directories to be checked.

  Args:
    root: The repository root where a .git directory must exist.

  Returns:
    A set of directories which contain sources managed by git.
  """
  git_source_directory = set()
  popen_out = os.popen('cd %s && git ls-files --full-name .' %
                       pipes.quote(root))
  for line in popen_out:
    dir_path = os.path.join(root, os.path.dirname(line))
    git_source_directory.add(dir_path)
  git_source_directory.add(root)
  return git_source_directory


def GetSvnRepositoryRoot(dir_path):
  """Returns the repository root for a directory.

  Args:
    dir_path: A directory where a .svn subdirectory should exist.

  Returns:
    The svn repository that contains dir_path or None.
  """
  svn_dir = os.path.join(dir_path, '.svn')
  if not os.path.isdir(svn_dir):
    return None
  popen_out = os.popen('cd %s && svn info' % pipes.quote(dir_path))
  for line in popen_out:
    if line.startswith('Repository Root: '):
      return line[len('Repository Root: '):].rstrip()
  return None


def main(argv):
  usage = """Usage: python %prog [--root <root>] [tocheck]
  tocheck  Specifies the directory, relative to root, to check. This defaults
           to "." so it checks everything.

Examples:
  python checkperms.py
  python checkperms.py --root /path/to/source chrome"""

  option_parser = optparse.OptionParser(usage=usage)
  option_parser.add_option('--root', dest='base_directory',
                           default=DEFAULT_BASE_DIRECTORY,
                           help='Specifies the repository root. This defaults '
                           'to %default relative to the script file, which '
                           'will normally be the repository root.')
  option_parser.add_option('-v', '--verbose', action='store_true',
                           help='Print debug logging')
  options, args = option_parser.parse_args()

  global VERBOSE
  if options.verbose:
    VERBOSE = True

  # Optional base directory of the repository.
  global BASE_DIRECTORY
  if (not options.base_directory or
      options.base_directory == DEFAULT_BASE_DIRECTORY):
    BASE_DIRECTORY = os.path.abspath(
        os.path.join(os.path.abspath(argv[0]), DEFAULT_BASE_DIRECTORY))
  else:
    BASE_DIRECTORY = os.path.abspath(argv[2])

  # Figure out which directory we have to check.
  if not args:
    # No directory to check specified, use the repository root.
    start_dir = BASE_DIRECTORY
  elif len(args) == 1:
    # Directory specified. Start here. It's supposed to be relative to the
    # base directory.
    start_dir = os.path.abspath(os.path.join(BASE_DIRECTORY, args[0]))
  else:
    # More than one argument, we don't handle this.
    option_parser.print_help()
    return 1

  print 'Using base directory:', BASE_DIRECTORY
  print 'Checking directory:', start_dir

  # The base directory should be lower case from here on since it will be used
  # for substring matching against whitelists that all assume lowercase, and we
  # compile on case-insensitive systems. Plus, we always use slashes here since
  # the include parsing code will also normalize to slashes.
  BASE_DIRECTORY = BASE_DIRECTORY.lower()
  BASE_DIRECTORY = BASE_DIRECTORY.replace('\\', '/')
  start_dir = start_dir.replace('\\', '/')

  success = True
  if os.path.exists(os.path.join(BASE_DIRECTORY, '.svn')):
    global SVN_REPO_URL
    SVN_REPO_URL = GetSvnRepositoryRoot(BASE_DIRECTORY)
    if not SVN_REPO_URL:
      print 'Cannot determine the SVN repo URL'
      success = False
  elif os.path.exists(os.path.join(BASE_DIRECTORY, '.git')):
    global IS_SVN
    IS_SVN = False
    global GIT_SOURCE_DIRECTORY
    GIT_SOURCE_DIRECTORY = GetGitSourceDirectory(BASE_DIRECTORY)
    if not GIT_SOURCE_DIRECTORY:
      print 'Cannot determine the list of GIT directories'
      success = False
  else:
    print 'Cannot determine the SCM used in %s' % BASE_DIRECTORY
    success = False

  if success:
    success = CheckDirectory(start_dir)
  if not success:
    print '\nFAILED\n'
    return 1
  print '\nSUCCESS\n'
  return 0


if '__main__' == __name__:
  sys.exit(main(sys.argv))