1 | # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) |
---|
2 | # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php |
---|
3 | """ |
---|
4 | This is a module to check the filesystem for the presence and |
---|
5 | permissions of certain files. It can also be used to correct the |
---|
6 | permissions (but not existance) of those files. |
---|
7 | |
---|
8 | Currently only supports Posix systems (with Posixy permissions). |
---|
9 | Permission stuff can probably be stubbed out later. |
---|
10 | """ |
---|
11 | import os |
---|
12 | |
---|
13 | def read_perm_spec(spec): |
---|
14 | """ |
---|
15 | Reads a spec like 'rw-r--r--' into a octal number suitable for |
---|
16 | chmod. That is characters in groups of three -- first group is |
---|
17 | user, second for group, third for other (all other people). The |
---|
18 | characters are r (read), w (write), and x (executable), though the |
---|
19 | executable can also be s (sticky). Files in sticky directories |
---|
20 | get the directories permission setting. |
---|
21 | |
---|
22 | Examples:: |
---|
23 | |
---|
24 | >>> print oct(read_perm_spec('rw-r--r--')) |
---|
25 | 0644 |
---|
26 | >>> print oct(read_perm_spec('rw-rwsr--')) |
---|
27 | 02664 |
---|
28 | >>> print oct(read_perm_spec('r-xr--r--')) |
---|
29 | 0544 |
---|
30 | >>> print oct(read_perm_spec('r--------')) |
---|
31 | 0400 |
---|
32 | """ |
---|
33 | total_mask = 0 |
---|
34 | # suid/sgid modes give this mask in user, group, other mode: |
---|
35 | set_bits = (04000, 02000, 0) |
---|
36 | pieces = (spec[0:3], spec[3:6], spec[6:9]) |
---|
37 | for i, (mode, set_bit) in enumerate(zip(pieces, set_bits)): |
---|
38 | mask = 0 |
---|
39 | read, write, exe = list(mode) |
---|
40 | if read == 'r': |
---|
41 | mask = mask | 4 |
---|
42 | elif read != '-': |
---|
43 | raise ValueError, ( |
---|
44 | "Character %r unexpected (should be '-' or 'r')" |
---|
45 | % read) |
---|
46 | if write == 'w': |
---|
47 | mask = mask | 2 |
---|
48 | elif write != '-': |
---|
49 | raise ValueError, ( |
---|
50 | "Character %r unexpected (should be '-' or 'w')" |
---|
51 | % write) |
---|
52 | if exe == 'x': |
---|
53 | mask = mask | 1 |
---|
54 | elif exe not in ('s', '-'): |
---|
55 | raise ValueError, ( |
---|
56 | "Character %r unexpected (should be '-', 'x', or 's')" |
---|
57 | % exe) |
---|
58 | if exe == 's' and i == 2: |
---|
59 | raise ValueError, ( |
---|
60 | "The 'other' executable setting cannot be suid/sgid ('s')") |
---|
61 | mask = mask << ((2-i)*3) |
---|
62 | if exe == 's': |
---|
63 | mask = mask | set_bit |
---|
64 | total_mask = total_mask | mask |
---|
65 | return total_mask |
---|
66 | |
---|
67 | modes = [ |
---|
68 | (04000, 'setuid bit', |
---|
69 | 'setuid bit: make contents owned by directory owner'), |
---|
70 | (02000, 'setgid bit', |
---|
71 | 'setgid bit: make contents inherit permissions from directory'), |
---|
72 | (01000, 'sticky bit', |
---|
73 | 'sticky bit: append-only directory'), |
---|
74 | (00400, 'read by owner', 'read by owner'), |
---|
75 | (00200, 'write by owner', 'write by owner'), |
---|
76 | (00100, 'execute by owner', 'owner can search directory'), |
---|
77 | (00040, 'allow read by group members', |
---|
78 | 'allow read by group members',), |
---|
79 | (00020, 'allow write by group members', |
---|
80 | 'allow write by group members'), |
---|
81 | (00010, 'execute by group members', |
---|
82 | 'group members can search directory'), |
---|
83 | (00004, 'read by others', 'read by others'), |
---|
84 | (00002, 'write by others', 'write by others'), |
---|
85 | (00001, 'execution by others', 'others can search directory'), |
---|
86 | ] |
---|
87 | |
---|
88 | exe_bits = [0100, 0010, 0001] |
---|
89 | exe_mask = 0111 |
---|
90 | full_mask = 07777 |
---|
91 | |
---|
92 | def mode_diff(filename, mode, **kw): |
---|
93 | """ |
---|
94 | Returns the differences calculated using ``calc_mode_diff`` |
---|
95 | """ |
---|
96 | cur_mode = os.stat(filename).st_mode |
---|
97 | return calc_mode_diff(cur_mode, mode, **kw) |
---|
98 | |
---|
99 | def calc_mode_diff(cur_mode, mode, keep_exe=True, |
---|
100 | not_set='not set: ', |
---|
101 | set='set: '): |
---|
102 | """ |
---|
103 | Gives the difference between the actual mode of the file and the |
---|
104 | given mode. If ``keep_exe`` is true, then if the mode doesn't |
---|
105 | include any executable information the executable information will |
---|
106 | simply be ignored. High bits are also always ignored (except |
---|
107 | suid/sgid and sticky bit). |
---|
108 | |
---|
109 | Returns a list of differences (empty list if no differences) |
---|
110 | """ |
---|
111 | for exe_bit in exe_bits: |
---|
112 | if mode & exe_bit: |
---|
113 | keep_exe = False |
---|
114 | diffs = [] |
---|
115 | isdir = os.path.isdir(filename) |
---|
116 | for bit, file_desc, dir_desc in modes: |
---|
117 | if keep_exe and bit in exe_bits: |
---|
118 | continue |
---|
119 | if isdir: |
---|
120 | desc = dir_desc |
---|
121 | else: |
---|
122 | desc = file_desc |
---|
123 | if (mode & bit) and not (cur_mode & bit): |
---|
124 | diffs.append(not_set + desc) |
---|
125 | if not (mode & bit) and (cur_mode & bit): |
---|
126 | diffs.append(set + desc) |
---|
127 | return diffs |
---|
128 | |
---|
129 | def calc_set_mode(cur_mode, mode, keep_exe=True): |
---|
130 | """ |
---|
131 | Calculates the new mode given the current node ``cur_mode`` and |
---|
132 | the mode spec ``mode`` and if ``keep_exe`` is true then also keep |
---|
133 | the executable bits in ``cur_mode`` if ``mode`` has no executable |
---|
134 | bits in it. Return the new mode. |
---|
135 | |
---|
136 | Examples:: |
---|
137 | |
---|
138 | >>> print oct(calc_set_mode(0775, 0644)) |
---|
139 | 0755 |
---|
140 | >>> print oct(calc_set_mode(0775, 0744)) |
---|
141 | 0744 |
---|
142 | >>> print oct(calc_set_mode(010600, 0644)) |
---|
143 | 010644 |
---|
144 | >>> print oct(calc_set_mode(0775, 0644, False)) |
---|
145 | 0644 |
---|
146 | """ |
---|
147 | for exe_bit in exe_bits: |
---|
148 | if mode & exe_bit: |
---|
149 | keep_exe = False |
---|
150 | # This zeros-out full_mask parts of the current mode: |
---|
151 | keep_parts = (cur_mode | full_mask) ^ full_mask |
---|
152 | if keep_exe: |
---|
153 | keep_parts = keep_parts | (cur_mode & exe_mask) |
---|
154 | new_mode = keep_parts | mode |
---|
155 | return new_mode |
---|
156 | |
---|
157 | def set_mode(filename, mode, **kw): |
---|
158 | """ |
---|
159 | Sets the mode on ``filename`` using ``calc_set_mode`` |
---|
160 | """ |
---|
161 | cur_mode = os.stat(filename).st_mode |
---|
162 | new_mode = calc_set_mode(cur_mode, mode, **kw) |
---|
163 | os.chmod(filename, new_mode) |
---|
164 | |
---|
165 | def calc_ownership_spec(spec): |
---|
166 | """ |
---|
167 | Calculates what a string spec means, returning (uid, username, |
---|
168 | gid, groupname), where there can be None values meaning no |
---|
169 | preference. |
---|
170 | |
---|
171 | The spec is a string like ``owner:group``. It may use numbers |
---|
172 | instead of user/group names. It may leave out ``:group``. It may |
---|
173 | use '-' to mean any-user/any-group. |
---|
174 | |
---|
175 | """ |
---|
176 | import grp |
---|
177 | import pwd |
---|
178 | user = group = None |
---|
179 | uid = gid = None |
---|
180 | if ':' in spec: |
---|
181 | user_spec, group_spec = spec.split(':', 1) |
---|
182 | else: |
---|
183 | user_spec, group_spec = spec, '-' |
---|
184 | if user_spec == '-': |
---|
185 | user_spec = '0' |
---|
186 | if group_spec == '-': |
---|
187 | group_spec = '0' |
---|
188 | try: |
---|
189 | uid = int(user_spec) |
---|
190 | except ValueError: |
---|
191 | uid = pwd.getpwnam(user_spec) |
---|
192 | user = user_spec |
---|
193 | else: |
---|
194 | if not uid: |
---|
195 | uid = user = None |
---|
196 | else: |
---|
197 | user = pwd.getpwuid(uid).pw_name |
---|
198 | try: |
---|
199 | gid = int(group_spec) |
---|
200 | except ValueError: |
---|
201 | gid = grp.getgrnam(group_spec) |
---|
202 | group = group_spec |
---|
203 | else: |
---|
204 | if not gid: |
---|
205 | gid = group = None |
---|
206 | else: |
---|
207 | group = grp.getgrgid(gid).gr_name |
---|
208 | return (uid, user, gid, group) |
---|
209 | |
---|
210 | def ownership_diff(filename, spec): |
---|
211 | """ |
---|
212 | Return a list of differences between the ownership of ``filename`` |
---|
213 | and the spec given. |
---|
214 | """ |
---|
215 | import grp |
---|
216 | import pwd |
---|
217 | diffs = [] |
---|
218 | uid, user, gid, group = calc_ownership_spec(spec) |
---|
219 | st = os.stat(filename) |
---|
220 | if uid and uid != st.st_uid: |
---|
221 | diffs.append('owned by %s (should be %s)' % |
---|
222 | (pwd.getpwuid(st.st_uid).pw_name, user)) |
---|
223 | if gid and gid != st.st_gid: |
---|
224 | diffs.append('group %s (should be %s)' % |
---|
225 | (grp.getgrgid(st.st_gid).gr_name, group)) |
---|
226 | return diffs |
---|
227 | |
---|
228 | def set_ownership(filename, spec): |
---|
229 | """ |
---|
230 | Set the ownership of ``filename`` given the spec. |
---|
231 | """ |
---|
232 | uid, user, gid, group = calc_ownership_spec(spec) |
---|
233 | st = os.stat(filename) |
---|
234 | if not uid: |
---|
235 | uid = st.st_uid |
---|
236 | if not gid: |
---|
237 | gid = st.st_gid |
---|
238 | os.chmod(filename, uid, gid) |
---|
239 | |
---|
240 | class PermissionSpec(object): |
---|
241 | """ |
---|
242 | Represents a set of specifications for permissions. |
---|
243 | |
---|
244 | Typically reads from a file that looks like this:: |
---|
245 | |
---|
246 | rwxrwxrwx user:group filename |
---|
247 | |
---|
248 | If the filename ends in /, then it expected to be a directory, and |
---|
249 | the directory is made executable automatically, and the contents |
---|
250 | of the directory are given the same permission (recursively). By |
---|
251 | default the executable bit on files is left as-is, unless the |
---|
252 | permissions specifically say it should be on in some way. |
---|
253 | |
---|
254 | You can use 'nomodify filename' for permissions to say that any |
---|
255 | permission is okay, and permissions should not be changed. |
---|
256 | |
---|
257 | Use 'noexist filename' to say that a specific file should not |
---|
258 | exist. |
---|
259 | |
---|
260 | Use 'symlink filename symlinked_to' to assert a symlink destination |
---|
261 | |
---|
262 | The entire file is read, and most specific rules are used for each |
---|
263 | file (i.e., a rule for a subdirectory overrides the rule for a |
---|
264 | superdirectory). Order does not matter. |
---|
265 | """ |
---|
266 | |
---|
267 | def __init__(self): |
---|
268 | self.paths = {} |
---|
269 | |
---|
270 | def parsefile(self, filename): |
---|
271 | f = open(filename) |
---|
272 | lines = f.readlines() |
---|
273 | f.close() |
---|
274 | self.parselines(lines, filename=filename) |
---|
275 | |
---|
276 | commands = {} |
---|
277 | |
---|
278 | def parselines(self, lines, filename=None): |
---|
279 | for lineindex, line in enumerate(lines): |
---|
280 | line = line.strip() |
---|
281 | if not line or line.startswith('#'): |
---|
282 | continue |
---|
283 | parts = line.split() |
---|
284 | command = parts[0] |
---|
285 | if command in self.commands: |
---|
286 | cmd = self.commands[command](*parts[1:]) |
---|
287 | else: |
---|
288 | cmd = self.commands['*'](*parts) |
---|
289 | self.paths[cmd.path] = cmd |
---|
290 | |
---|
291 | def check(self): |
---|
292 | action = _Check(self) |
---|
293 | self.traverse(action) |
---|
294 | |
---|
295 | def fix(self): |
---|
296 | action = _Fixer(self) |
---|
297 | self.traverse(action) |
---|
298 | |
---|
299 | def traverse(self, action): |
---|
300 | paths = self.paths_sorted() |
---|
301 | checked = {} |
---|
302 | for path, checker in list(paths)[::-1]: |
---|
303 | self.check_tree(action, path, paths, checked) |
---|
304 | for path, checker in paths: |
---|
305 | if path not in checked: |
---|
306 | action.noexists(path, checker) |
---|
307 | |
---|
308 | def traverse_tree(self, action, path, paths, checked): |
---|
309 | if path in checked: |
---|
310 | return |
---|
311 | self.traverse_path(action, path, paths, checked) |
---|
312 | if os.path.isdir(path): |
---|
313 | for fn in os.listdir(path): |
---|
314 | fn = os.path.join(path, fn) |
---|
315 | self.traverse_tree(action, fn, paths, checked) |
---|
316 | |
---|
317 | def traverse_path(self, action, path, paths, checked): |
---|
318 | checked[path] = None |
---|
319 | for check_path, checker in paths: |
---|
320 | if path.startswith(check_path): |
---|
321 | action.check(check_path, checker) |
---|
322 | if not checker.inherit: |
---|
323 | break |
---|
324 | |
---|
325 | def paths_sorted(self): |
---|
326 | paths = self.paths.items() |
---|
327 | paths.sort(lambda a, b: -cmp(len(a[0]), len(b[0]))) |
---|
328 | |
---|
329 | class _Rule(object): |
---|
330 | class __metaclass__(type): |
---|
331 | def __new__(meta, class_name, bases, d): |
---|
332 | cls = type.__new__(meta, class_name, bases, d) |
---|
333 | PermissionSpec.commands[cls.__name__] = cls |
---|
334 | return cls |
---|
335 | |
---|
336 | inherit = False |
---|
337 | def noexists(self): |
---|
338 | return ['Path %s does not exist' % path] |
---|
339 | |
---|
340 | class _NoModify(_Rule): |
---|
341 | |
---|
342 | name = 'nomodify' |
---|
343 | |
---|
344 | def __init__(self, path): |
---|
345 | self.path = path |
---|
346 | |
---|
347 | def fix(self, path): |
---|
348 | pass |
---|
349 | |
---|
350 | class _NoExist(_Rule): |
---|
351 | |
---|
352 | name = 'noexist' |
---|
353 | |
---|
354 | def __init__(self, path): |
---|
355 | self.path = path |
---|
356 | |
---|
357 | def check(self, path): |
---|
358 | return ['Path %s should not exist' % path] |
---|
359 | |
---|
360 | def noexists(self, path): |
---|
361 | return [] |
---|
362 | |
---|
363 | def fix(self, path): |
---|
364 | # @@: Should delete? |
---|
365 | pass |
---|
366 | |
---|
367 | class _SymLink(_Rule): |
---|
368 | |
---|
369 | name = 'symlink' |
---|
370 | inherit = True |
---|
371 | |
---|
372 | def __init__(self, path, dest): |
---|
373 | self.path = path |
---|
374 | self.dest = dest |
---|
375 | |
---|
376 | def check(self, path): |
---|
377 | assert path == self.path, ( |
---|
378 | "_Symlink should only be passed specific path %s (not %s)" |
---|
379 | % (self.path, path)) |
---|
380 | try: |
---|
381 | link = os.path.readlink(path) |
---|
382 | except OSError: |
---|
383 | if e.errno != 22: |
---|
384 | raise |
---|
385 | return ['Path %s is not a symlink (should point to %s)' |
---|
386 | % (path, self.dest)] |
---|
387 | if link != self.dest: |
---|
388 | return ['Path %s should symlink to %s, not %s' |
---|
389 | % (path, self.dest, link)] |
---|
390 | return [] |
---|
391 | |
---|
392 | def fix(self, path): |
---|
393 | assert path == self.path, ( |
---|
394 | "_Symlink should only be passed specific path %s (not %s)" |
---|
395 | % (self.path, path)) |
---|
396 | if not os.path.exists(path): |
---|
397 | os.symlink(path, self.dest) |
---|
398 | else: |
---|
399 | # @@: This should correct the symlink or something: |
---|
400 | print 'Not symlinking %s' % path |
---|
401 | |
---|
402 | class _Permission(_Rule): |
---|
403 | |
---|
404 | name = '*' |
---|
405 | |
---|
406 | def __init__(self, perm, owner, dir): |
---|
407 | self.perm_spec = read_perm_spec(perm) |
---|
408 | self.owner = owner |
---|
409 | self.dir = dir |
---|
410 | |
---|
411 | def check(self, path): |
---|
412 | return mode_diff(path, self.perm_spec) |
---|
413 | |
---|
414 | def fix(self, path): |
---|
415 | set_mode(path, self.perm_spec) |
---|
416 | |
---|
417 | class _Strategy(object): |
---|
418 | |
---|
419 | def __init__(self, spec): |
---|
420 | self.spec = spec |
---|
421 | |
---|
422 | class _Check(_Strategy): |
---|
423 | |
---|
424 | def noexists(self, path, checker): |
---|
425 | checker.noexists(path) |
---|
426 | |
---|
427 | def check(self, path, checker): |
---|
428 | checker.check(path) |
---|
429 | |
---|
430 | class _Fixer(_Strategy): |
---|
431 | |
---|
432 | def noexists(self, path, checker): |
---|
433 | pass |
---|
434 | |
---|
435 | def check(self, path, checker): |
---|
436 | checker.fix(path) |
---|
437 | |
---|
438 | if __name__ == '__main__': |
---|
439 | import doctest |
---|
440 | doctest.testmod() |
---|
441 | |
---|