hg_hooks/blacklist/blacklist.py
changeset 143 6e81c130aa29
equal deleted inserted replaced
135:47849267b4d1 143:6e81c130aa29
       
     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             }