|
1 #!/usr/bin/env python |
|
2 # |
|
3 # Copyright (C) 2010 Mozilla Foundation |
|
4 # Copyright (C) 2010 Symbian Foundation |
|
5 # |
|
6 # This program is free software; you can redistribute it and/or |
|
7 # modify it under the terms of the GNU General Public License |
|
8 # as published by the Free Software Foundation; either version 2 |
|
9 # of the License, or (at your option) any later version. |
|
10 # |
|
11 # This program is distributed in the hope that it will be useful, |
|
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 # GNU General Public License for more details. |
|
15 # |
|
16 # You should have received a copy of the GNU General Public License |
|
17 # along with this program; if not, write to the Free Software |
|
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
19 |
|
20 # Initial Contributors: |
|
21 # Pat Downey <patd@symbian.org> |
|
22 # |
|
23 # Contributors: |
|
24 # Your name here? |
|
25 # |
|
26 # Description: |
|
27 # |
|
28 # An extension to mercurial that adds the ability to specify a blacklist for |
|
29 # a repository. That is to deny a changeset from being pushed/pulled/unbundled |
|
30 # if it matches one of a set of patterns. |
|
31 # |
|
32 # At present it can deny nodes based on their changeset id, a regular expression |
|
33 # matched against the user field, or a regular expression matched against the |
|
34 # changeset's file list. |
|
35 # |
|
36 # Note: With the regular expression rules, if you want to match a string anywhere |
|
37 # with in a string, e.g. create a rule against files within directories called |
|
38 # 'internal' the rule would need to be ..*/internal/.*'. That is you need to be |
|
39 # explicit in specifying a set of any characters otherwise it will perform a |
|
40 # direct string comparison. # |
|
41 # |
|
42 # Requires sqlite extension (included in python 2.5 onwards) |
|
43 # * Available for python 2.4 in python-sqlite2 package on RHEL5.2+ |
|
44 # |
|
45 # Ideas for implementation came strongly from: |
|
46 # http://hg.mozilla.org/users/bsmedberg_mozilla.com/hghooks/file/tip/mozhghooks/pushlog.py |
|
47 # |
|
48 # |
|
49 |
|
50 '''manage repository changeset blacklist |
|
51 |
|
52 ''' |
|
53 |
|
54 from mercurial import demandimport |
|
55 |
|
56 demandimport.disable() |
|
57 try: |
|
58 import sqlite3 as sqlite |
|
59 except ImportError: |
|
60 from pysqlite2 import dbapi2 as sqlite |
|
61 demandimport.enable() |
|
62 |
|
63 import binascii |
|
64 import os |
|
65 import os.path |
|
66 import re |
|
67 import stat |
|
68 import sys |
|
69 import time |
|
70 from datetime import datetime |
|
71 |
|
72 |
|
73 # changeset identifier 12-40 hex chars |
|
74 NODE_RE='^[0-9|a-f]{12,40}' |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 def blacklist(ui,repo,*args,**opts): |
|
80 '''manage repository changeset blacklist |
|
81 |
|
82 This extension is used to manage a blacklist for the repository. |
|
83 Can blacklist changesets by changeset id, and regular expressions against |
|
84 the user field of a changeset and also a changesets file list. |
|
85 |
|
86 Current rules can be viewed using the [-l|--list] operation. |
|
87 |
|
88 Each modification to a blacklist is logged. These can be viewed using the |
|
89 --auditlog operation. |
|
90 |
|
91 Each time a changeset is blocked/denied it's logged. These can be viewed |
|
92 using the --blocklog operation. |
|
93 |
|
94 Types of changeset blacklist rules can be defined implicitly or explicitly: |
|
95 |
|
96 If a rule definition contains between 12 and 40 hexadecimal characters |
|
97 it is assumed to be a rule matched against changeset id. Can be set |
|
98 explicitly set with the -n flag to the --add operation. |
|
99 |
|
100 If a rule definition contains a '@' it is assumed to be a rule matched |
|
101 against a changeset's user property. Can be set explicitly with |
|
102 the -u flag to the --add operation. |
|
103 |
|
104 Otherwise the rule is assumed to be matched against a changeset's file |
|
105 list. Can be set explicitly with the -f flag to the --add operation. |
|
106 |
|
107 When this extension is enabled a hook is also added to the |
|
108 'pretxnchangegroup' action that will block any incoming changesets |
|
109 (via pull/push/unbundle) if they are blacklisted. |
|
110 It won't block any local commits. |
|
111 ''' |
|
112 conn = openconn(ui, repo ) |
|
113 if 'list' in opts and opts['list'] : |
|
114 listblacklistrule(ui,conn,args,opts) |
|
115 elif 'blocklog' in opts and opts['blocklog'] : |
|
116 listblacklistblocklog(ui,conn,args,opts) |
|
117 elif 'auditlog' in opts and opts['auditlog'] : |
|
118 listblacklistauditlog(ui,conn,args,opts) |
|
119 elif 'enable' in opts and opts['enable'] : |
|
120 enableblacklistrule(ui,conn,args,opts) |
|
121 elif 'disable' in opts and opts['disable'] : |
|
122 disableblacklistrule(ui,conn,args,opts) |
|
123 elif 'remove' in opts and opts['remove'] : |
|
124 removeblacklistrule(ui,conn,args,opts) |
|
125 elif 'add' in opts and opts['add'] : |
|
126 addblacklistrule(ui,conn,args,opts) |
|
127 else : |
|
128 ui.warn( 'invalid operation specified\n' ) |
|
129 |
|
130 conn.close( ) |
|
131 |
|
132 ####### Database setup methods |
|
133 # this part derived from mozilla's pushlog.py hook |
|
134 def openconn(ui,repo): |
|
135 blacklistdb = os.path.join(repo.path, 'blacklist.db') |
|
136 createdb = False |
|
137 if not os.path.exists(blacklistdb): |
|
138 createdb = True |
|
139 conn = sqlite.connect(blacklistdb) |
|
140 if not createdb and not schemaexists(conn): |
|
141 createdb = True |
|
142 if createdb: |
|
143 createblacklistdb(ui,conn) |
|
144 st = os.stat(blacklistdb) |
|
145 os.chmod(blacklistdb, st.st_mode | stat.S_IWGRP) |
|
146 |
|
147 return conn |
|
148 |
|
149 # Derived from mozilla's pushlog hook |
|
150 def schemaexists(conn): |
|
151 return 3 == conn.execute("SELECT COUNT(*) FROM SQLITE_MASTER WHERE name IN ( ?, ?, ?)" , ['blacklist_rule','blacklist_auditlog','blacklist_blocklog']).fetchone()[0] |
|
152 |
|
153 # Derived from mozilla's pushlog hook |
|
154 def createblacklistdb(ui,conn): |
|
155 # record of different blacklist rule, type should be either 'node' or 'file' or 'user' |
|
156 # 'node' - compare pattern with changeset identifier |
|
157 # 'file' - used as regular expression against changeset file manifest |
|
158 # 'user' - used as regular expression against changeset author/user |
|
159 # (id,pattern,type,enabled) |
|
160 conn.execute("CREATE TABLE IF NOT EXISTS blacklist_rule (id INTEGER PRIMARY KEY AUTOINCREMENT, pattern TEXT, type TEXT, enabled INTEGER,comment TEXT)") |
|
161 conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS blacklist_rule_idx ON blacklist_rule (pattern,type)" ) |
|
162 |
|
163 # records additions and modifications to the blacklist_rule table |
|
164 # (id, operation, rule_id, user, date, comment) |
|
165 conn.execute("CREATE TABLE IF NOT EXISTS blacklist_auditlog (id INTEGER PRIMARY KEY AUTOINCREMENT, operation TEXT, rule_id INTEGER, user TEXT, date INTEGER, comment TEXT)") |
|
166 conn.execute("CREATE INDEX IF NOT EXISTS blacklist_auditlog_rule_idx ON blacklist_auditlog (rule_id)" ) |
|
167 |
|
168 # log attempted pushes and the http users trying to push a blocked changeset |
|
169 # (id,rule_id,cset_id, cset_user, cset_desc, user,date) |
|
170 conn.execute("CREATE TABLE IF NOT EXISTS blacklist_blocklog (id INTEGER PRIMARY KEY AUTOINCREMENT, rule_id INTEGER, cset_id TEXT, cset_user TEXT, cset_desc TEXT, user TEXT, date INTEGER)") |
|
171 conn.execute("CREATE INDEX IF NOT EXISTS blacklist_blocklog_rule_idx ON blacklist_blocklog (rule_id)" ) |
|
172 |
|
173 conn.commit() |
|
174 |
|
175 |
|
176 # Methods for extension commands |
|
177 def __getblacklistruletype( ui, pattern, opts ): |
|
178 type=None |
|
179 |
|
180 if opts['nodeType'] : |
|
181 type = 'node' |
|
182 elif opts['fileType'] : |
|
183 type = 'file' |
|
184 elif opts['userType'] : |
|
185 type = 'user' |
|
186 |
|
187 # try and work out type of blacklist if none specified |
|
188 # default to regexp |
|
189 if type == None : |
|
190 if re.match( NODE_RE, pattern ) : |
|
191 type = 'node' |
|
192 elif '@' in pattern : |
|
193 type = 'user' |
|
194 else : |
|
195 type = 'file' |
|
196 ui.note( 'type implicitly set to \'%s\'\n' % type ) |
|
197 |
|
198 return type |
|
199 |
|
200 def addblacklistrule(ui,conn,args,opts): |
|
201 ret = 1 |
|
202 if len(args) == 1 : |
|
203 createrule = True |
|
204 pattern = args[0] |
|
205 |
|
206 type = __getblacklistruletype( ui, pattern, opts ) |
|
207 |
|
208 if type == 'node' : |
|
209 # if pattern has been specified as a node type |
|
210 # check that pattern is a valid node |
|
211 if not re.match( NODE_RE, pattern ) : |
|
212 ui.warn( 'node should be 12 or 40 characters.\n' ) |
|
213 createrule = False |
|
214 |
|
215 if createrule : |
|
216 comment = None |
|
217 |
|
218 if 'desc' in opts and opts['desc'] : |
|
219 if opts['desc'] != '' : |
|
220 comment = opts['desc'] |
|
221 |
|
222 insertblacklistrule(ui,conn,pattern,type, comment=comment) |
|
223 else : |
|
224 ui.warn( 'missing pattern argument.\n' ) |
|
225 |
|
226 return ret |
|
227 |
|
228 def removeblacklistrule(ui,conn,args,opts): |
|
229 if len(args) == 1 : |
|
230 deleteblacklistrule(ui,conn,args[0]) |
|
231 else : |
|
232 ui.warn( 'rule id argument required.\n' ) |
|
233 |
|
234 return 0 |
|
235 |
|
236 def disableblacklistrule(ui,conn,args,opts): |
|
237 if len(args) == 1 : |
|
238 updateblacklistrule(ui,conn,args[0],False) |
|
239 else : |
|
240 ui.warn( 'rule id argument required.\n' ) |
|
241 |
|
242 return 0 |
|
243 |
|
244 def enableblacklistrule(ui,conn,args,opts): |
|
245 if len(args) == 1 : |
|
246 updateblacklistrule(ui,conn,args[0],True) |
|
247 else : |
|
248 ui.warn( 'rule id argument required.\n' ) |
|
249 |
|
250 return 0 |
|
251 |
|
252 def listblacklistrule(ui,conn,args,opts): |
|
253 if len(args) in (0,1) : |
|
254 res = selectblacklistrule(ui,conn,args) |
|
255 |
|
256 printblacklist(ui,res) |
|
257 else : |
|
258 ui.warn( 'too many arguments.\n' ) |
|
259 |
|
260 return 0 |
|
261 |
|
262 def listblacklistauditlog(ui,conn,args,opts): |
|
263 if len(args) in (0,1) : |
|
264 res = selectblacklistauditlog(ui, conn, args ) |
|
265 |
|
266 printauditlog(ui,res) |
|
267 else : |
|
268 ui.warn( 'too many arguments.\n' ) |
|
269 |
|
270 return 0 |
|
271 |
|
272 def listblacklistblocklog(ui,conn,args,opts): |
|
273 if len(args) in (0,1) : |
|
274 res = selectblacklistblocklog(ui, conn, args ) |
|
275 |
|
276 printblocklog(ui,res) |
|
277 else : |
|
278 ui.warn( 'too many arguments.\n' ) |
|
279 |
|
280 return 0 |
|
281 |
|
282 def insertblacklistaudit(ui, conn, operation, rule_id, comment=None ): |
|
283 user = __getenvuser( ) |
|
284 audit_date = int(time.time()) |
|
285 |
|
286 audit_sql = 'INSERT INTO blacklist_auditlog ( operation, rule_id, user, date, comment ) VALUES ( ?, ?, ?, ?, ? )' |
|
287 conn.execute( audit_sql, (operation, rule_id, user, audit_date, comment ) ) |
|
288 |
|
289 def insertblacklistrule(ui, conn, pattern, type, enabled=True, comment=None): |
|
290 rule_sql = 'INSERT INTO blacklist_rule ( pattern, type, enabled,comment ) VALUES ( ?, ?, ?, ? )' |
|
291 |
|
292 res = conn.execute( rule_sql, (pattern,type,enabled,comment) ) |
|
293 rule_id= res.lastrowid |
|
294 |
|
295 insertblacklistaudit(ui, conn, 'add', rule_id,comment=comment ) |
|
296 |
|
297 conn.commit( ) |
|
298 |
|
299 def __getenvuser( ): |
|
300 # look at REMOTE_USER first |
|
301 # then look at LOGUSER |
|
302 # then look at USER |
|
303 for e in ['REMOTE_USER','SUDO_USER','LOGUSER','USER'] : |
|
304 if e in os.environ : |
|
305 user = '%s:%s' %( e, os.environ.get( e ) ) |
|
306 break |
|
307 |
|
308 return user |
|
309 |
|
310 def insertblacklistblocklog( ui, conn, rule, ctx ): |
|
311 # (id, rule_id, user, date) |
|
312 rule_id=rule[0] |
|
313 |
|
314 log_user = __getenvuser( ) |
|
315 audit_date = int(time.time()) |
|
316 |
|
317 ctx_node = binascii.hexlify(ctx.node()) |
|
318 ctx_user = ctx.user() |
|
319 ctx_desc = ctx.description() |
|
320 |
|
321 log_sql = 'INSERT INTO blacklist_blocklog (rule_id,user,date,cset_id,cset_user,cset_desc) VALUES (?,?,?,?,?,?)' |
|
322 conn.execute( log_sql, (rule_id,log_user,audit_date, ctx_node, ctx_user, ctx_desc)) |
|
323 conn.commit() |
|
324 |
|
325 def updateblacklistrule(ui, conn, rule_id, enabled): |
|
326 rule_sql = 'UPDATE blacklist_rule SET enabled=? WHERE id=?' |
|
327 |
|
328 conn.execute( rule_sql, [enabled, rule_id] ) |
|
329 |
|
330 insertblacklistaudit(ui, conn, 'update', rule_id, 'enabled=%s' % enabled ) |
|
331 |
|
332 conn.commit( ) |
|
333 |
|
334 def deleteblacklistrule(ui, conn, rule_id ): |
|
335 if rule_id != None : |
|
336 res = selectblacklistrule(ui, conn, rule_id ) |
|
337 processed = False |
|
338 for (id,pattern,type,enabled,comment) in res : |
|
339 comment = 'deleted: pattern=%s, type=%s, enabled=%s' % (pattern, type, enabled) |
|
340 |
|
341 rule_sql = 'DELETE FROM blacklist_rule WHERE id=?' |
|
342 conn.execute( rule_sql, [rule_id] ) |
|
343 |
|
344 insertblacklistaudit(ui, conn, 'delete', rule_id, comment) |
|
345 |
|
346 conn.commit( ) |
|
347 processed = True |
|
348 |
|
349 if not processed : |
|
350 ui.warn( 'no matching blacklist rule found with id %s\n' % rule_id ) |
|
351 else : |
|
352 ui.warn( 'no rule id specified\n' ) |
|
353 |
|
354 def selectblacklistrule(ui, conn, rule_id ): |
|
355 # (id, operation, rule_id, user, date, comment) |
|
356 if rule_id : |
|
357 rule_sql = 'SELECT id,pattern,type,enabled,comment FROM blacklist_rule WHERE id=? ORDER BY id ASC' |
|
358 res = conn.execute( rule_sql, rule_id ) |
|
359 else : |
|
360 rule_sql = 'SELECT id,pattern,type,enabled,comment FROM blacklist_rule ORDER BY id ASC' |
|
361 res = conn.execute( rule_sql ) |
|
362 |
|
363 return res |
|
364 |
|
365 def selectblacklistauditlog(ui,conn,rule_id=None) : |
|
366 # (id, operation, rule_id, user, date, comment) |
|
367 if rule_id : |
|
368 rule_sql = 'SELECT id,operation,rule_id,user,date,comment FROM blacklist_auditlog WHERE rule_id=? ORDER BY date ASC' |
|
369 res = conn.execute( rule_sql, rule_id ) |
|
370 else : |
|
371 rule_sql = 'SELECT id,operation,rule_id,user,date,comment FROM blacklist_auditlog ORDER BY date ASC' |
|
372 res = conn.execute( rule_sql ) |
|
373 |
|
374 return res |
|
375 |
|
376 def selectblacklistblocklog(ui,conn,rule_id=None) : |
|
377 # (id, rule_id, node, user, date) |
|
378 if rule_id : |
|
379 rule_sql = 'SELECT id,rule_id,cset_id,cset_user,cset_desc,user,date FROM blacklist_blocklog WHERE rule_id=? ORDER BY date ASC' |
|
380 res = conn.execute( rule_sql, rule_id ) |
|
381 else : |
|
382 rule_sql = 'SELECT id,rule_id,cset_id,cset_user,cset_desc,user,date FROM blacklist_blocklog ORDER BY date ASC' |
|
383 res = conn.execute( rule_sql ) |
|
384 |
|
385 return res |
|
386 |
|
387 def printblacklist(ui,res): |
|
388 for r in res : |
|
389 (id,pattern,type,enabled,comment) = r |
|
390 |
|
391 if enabled == 1 : |
|
392 enabled = True |
|
393 elif enabled == 0 : |
|
394 enabled = False |
|
395 |
|
396 ui.write( 'rule: %d:%s\n' % (id,type) ) |
|
397 ui.write( 'pattern: %s\n' % pattern ) |
|
398 ui.write( 'enabled: %s\n' % enabled ) |
|
399 if comment : |
|
400 ui.write( 'comment: %s\n' % comment ) |
|
401 ui.write( '\n' ) |
|
402 |
|
403 def printauditlog(ui,res): |
|
404 for r in res : |
|
405 (id, operation, rule_id, user, date, comment) = r |
|
406 |
|
407 date = datetime.utcfromtimestamp(date).isoformat() |
|
408 |
|
409 if not comment : |
|
410 comment = '' |
|
411 |
|
412 ui.write( 'date: %s\n' % date ) |
|
413 ui.write( 'operation: %s\n' % operation ) |
|
414 ui.write( 'user: %s\n' % user ) |
|
415 ui.write( 'rule: %s\n' % rule_id ) |
|
416 ui.write( 'comment: %s\n' % comment ) |
|
417 ui.write( '\n' ) |
|
418 |
|
419 def printblocklog(ui,res): |
|
420 for r in res : |
|
421 (id, rule_id, cset_id, cset_user, cset_desc, user, date) = r |
|
422 |
|
423 date = datetime.utcfromtimestamp(date).isoformat() |
|
424 |
|
425 ui.write( 'cset: %s\n' % cset_id ) |
|
426 ui.write( 'cset-user: %s\n' % cset_user) |
|
427 ui.write( 'cset-desc: %s\n' % cset_desc ) |
|
428 ui.write( 'rule: %s\n' % rule_id ) |
|
429 ui.write( 'date: %s\n' % date ) |
|
430 ui.write( 'user: %s\n' % user ) |
|
431 |
|
432 ui.write( '\n' ) |
|
433 |
|
434 |
|
435 # Hook specific functions follow |
|
436 |
|
437 def excludecsetbyfile(ctx,pattern): |
|
438 exclude = False |
|
439 |
|
440 file_re = re.compile( '%s' % pattern, re.I ) |
|
441 for f in ctx.files() : |
|
442 if file_re.match( f ) : |
|
443 exclude = True |
|
444 break |
|
445 |
|
446 return exclude |
|
447 |
|
448 def excludecsetbynode(ctx,pattern): |
|
449 exclude = False |
|
450 |
|
451 node = binascii.hexlify(ctx.node()) |
|
452 |
|
453 if node.startswith( pattern ) : |
|
454 exclude = True |
|
455 |
|
456 return exclude |
|
457 |
|
458 def excludecsetbyuser(ctx,pattern): |
|
459 exclude = False |
|
460 userStr = ctx.user() |
|
461 |
|
462 user_re = re.compile( '^.*%s.*$' % pattern, re.I ) |
|
463 if user_re.match( userStr ) : |
|
464 exclude = True |
|
465 |
|
466 return exclude |
|
467 |
|
468 def excludeblacklistcset(ui,conn,ctx): |
|
469 |
|
470 bl_sql = 'SELECT id,pattern,type FROM blacklist_rule WHERE enabled=1' |
|
471 res = conn.execute( bl_sql ) |
|
472 |
|
473 (exclude,rule) = (False,None) |
|
474 |
|
475 for (id,pattern,type) in res : |
|
476 if type == 'node' : |
|
477 exclude = excludecsetbynode(ctx,pattern) |
|
478 elif type == 'user' : |
|
479 exclude = excludecsetbyuser(ctx,pattern) |
|
480 elif type == 'file' : |
|
481 exclude = excludecsetbyfile(ctx,pattern) |
|
482 else : |
|
483 ui.warn('unrecognised rule type \'%s\'' % type ) |
|
484 |
|
485 if exclude : |
|
486 rule = (id,pattern,type) |
|
487 break |
|
488 |
|
489 return (exclude,rule) |
|
490 |
|
491 # The hook method that is used to block bad changesets from being introduced |
|
492 # to the current repository |
|
493 def pretxnchangegroup(ui,repo,hooktype,node,**args): |
|
494 start = repo[node].rev() |
|
495 end = len(repo) |
|
496 |
|
497 conn = openconn(ui, repo ) |
|
498 |
|
499 blocked = False |
|
500 |
|
501 for rev in xrange(start, end): |
|
502 ctx = repo[rev] |
|
503 (blocked,rule) = excludeblacklistcset( ui, conn, ctx) |
|
504 |
|
505 if blocked : |
|
506 insertblacklistblocklog( ui, conn, rule, ctx ) |
|
507 (id,pattern,type) = rule |
|
508 ui.write( 'blocked: cset %s in changegroup blocked by blacklist\n' % str(ctx) ) |
|
509 ui.write( 'blocked-reason: %s matched against \'%s\'\n' % ( type,pattern )) |
|
510 break |
|
511 |
|
512 conn.close( ) |
|
513 |
|
514 return blocked |
|
515 |
|
516 def setupblacklisthook(ui): |
|
517 ui.setconfig('hooks', 'pretxnchangegroup.blacklist', pretxnchangegroup) |
|
518 |
|
519 def reposetup(ui,repo): |
|
520 # print 'in blacklist reposetup' |
|
521 setupblacklisthook( ui ) |
|
522 |
|
523 def uisetup(ui): |
|
524 # print 'in blacklist uisetup' |
|
525 setupblacklisthook( ui ) |
|
526 |
|
527 cmdtable = { |
|
528 'blacklist': (blacklist, |
|
529 [ |
|
530 ('l','list',None,'list blacklist entries'), |
|
531 ('','blocklog',None,'list blocked changesets'), |
|
532 ('','auditlog',None,'show audit log for blacklist'), |
|
533 ('a','add',None,'add node to blacklist'), |
|
534 ('d','disable',None,'disable blacklist rule'), |
|
535 ('e','enable',None,'enable blacklist rule'), |
|
536 ('r','remove',None,'remove node from blacklist'), |
|
537 ('n','nodeType',False,'parse argument as node to blacklist'), |
|
538 ('f','fileType',False,'parse argument as file path to blacklist'), |
|
539 ('u','userType',False,'parse argument as user regexp to blacklist'), |
|
540 ('','desc','','comment to attach to rule')], |
|
541 "") |
|
542 } |