import repository from arizona
[raven.git] / apps / stork / storktrustedpackagesparse.py
1
2 #!/usr/bin/env python
3 """
4 <Program Name>
5    storktrustedpackagesparse.py
6
7 <Author>
8    Justin Cappos
9
10 <Purpose>
11    Trustedpackages parser
12 """
13
14 #            [option, long option,                   variable,   action,        data,     default,                                    metavar,     description]
15 """arizonaconfig
16     options=[["",     "--trustedpackagesdtd",        "tpdtd",      "store",       "string", "/usr/local/stork/bin/trustedpackages.dtd", "FILENAME",  "The trustedpackages DTD file"],
17              ["",     "--missingtp",                 "missingtp",  "store",       "string", "exit",                                     None,        "action to take when missing a tpfile: [exit|ignore|denyall|deny]"]]
18     includes=[]
19 """
20
21 import sys
22 import arizonaxml
23 import arizonaconfig
24 import arizonacrypt
25 import fnmatch
26 import os
27 import arizonatransfer
28 import arizonageneral
29 import arizonareport
30 import storkrepolist
31 import storklog
32
33 glo_tpcacheentry = None
34 glo_last_repo_printed = None
35
36 # exception generated when a tp error occurs
37 TPFileError = "tpfileerror"
38
39 FILE_EXTENSION = ".tpfile"
40
41 # Trusted package entry dictionary entries:
42 #    kind            either 'PACKAGE' for packages, or 'USER' for user entries
43 #    pattern         pattern to match
44 #    hash            hash to match, for file entries
45 #    tpfilename      for USER entries, the name of the trusted packages file to read
46 #    action          'allow', 'deny', or 'any'
47 #    order-by        'default' or 'timestamp'
48 #    timestamp       as set by storkutil
49 #    number          ordinal number of entry (0, 1, 2, ...)
50 #    parent-number   ordinal number of parent, used as primary sort key
51 #    filters         list of pattern filters from enclosing USER entries
52 #    tags            list of tags that are applied to the package
53 #    mantags         list of mandatory tags (that the user must specify)
54 #    requiretags     for USER entries, a list of tags that must be present in the PACKAGE entries
55
56 class __TrustedPackagesApplication(arizonaxml.XMLApplication):
57    """
58    <Purpose> 
59       This class parses the trustedpackages file.
60
61    TODO finish comment         
62    """
63    def __init__(self,keyname,parents):
64       """
65       <Purpose>
66          (Comment retained from John's code -- likely retained from
67          example)
68
69       TODO finish comment
70       """
71       arizonaxml.XMLApplication.__init__(self)
72
73       # Dictionary of tp info
74       arizonaxml.CreateAttr(self, 'tp_dict', {})
75
76       # Current user
77       arizonaxml.CreateAttr(self, 'user', keyname)
78
79       # Parent list, for cycle detection. It contains the names of all tpfiles
80       # that are parents of this tpfile. It is considered an error to recurse
81       # from this tpfile to one that is already in the list.
82       arizonaxml.CreateAttr(self, 'parent_list', parents)
83
84    def include_tpfile(self, username, publickey):
85       """
86       <Purpose>
87          TODO fix comment
88       """
89       global glo_tpcacheentry
90
91       publickey_string = publickey.string
92       publickey_string_old = arizonacrypt.publickey_sl_to_fnstring_compat(publickey.sl)
93       publickey_hash = publickey.hash
94       longname = username + "." + publickey_string
95
96       # If we're missing this user's trusted packages file parse it
97       if not self.tp_dict.has_key(longname):
98          new_parents = self.parent_list[:]
99          new_parents.append(self.user)
100
101          # Get the name of the new file
102          possible_tpfilenames = []
103          possible_tpfilenames.append(username + "." + publickey_hash + FILE_EXTENSION)
104          possible_tpfilenames.append(username + "." + publickey_string_old + FILE_EXTENSION)
105
106          # SMB: choose between long and short key versions based on date
107          (tpfilename, tp_repo) = storkrepolist.find_file_list("tpfiles", possible_tpfilenames, None, publickey_string)
108
109          if (glo_tpcacheentry != None):
110              glo_tpcacheentry.add_include("tpfiles", possible_tpfilenames, publickey_string, tpfilename)
111
112          if not tpfilename:
113             raise TPFileError, "missing"
114
115          # Parse the new file
116          self.tp_dict.update(
117              TrustedPackagesFileParse(arizonaconfig.get_option("tpdtd"),
118              tpfilename,
119              tp_repo,
120              publickey_string,
121              longname,
122              new_parents))
123
124    def handle_start_tag(self, tag, attrs):
125       """
126       <Purpose>
127          TODO fix comment
128       """
129       #line = self.loc.get_line()
130       #file = self.loc.get_current_sysid()
131
132       if tag == "TRUSTEDPACKAGES":
133          # Make an entry for the username.publickey in the dict
134          self.tp_dict[self.user] = []
135
136       elif (tag == "PACKAGE") or (tag == "FILE"):
137          # Add the file action to the list...
138          tpentry = {}
139          tpentry['kind'] = "PACKAGE"
140          tpentry['pattern'] = attrs.get("PATTERN")
141          tpentry['hash'] = attrs.get("HASH")
142          tpentry['action'] = attrs.get("ACTION", "unspecified").lower().strip()
143          tpentry['timestamp'] = attrs.get("TIMESTAMP")
144          tpentry['provides'] = attrs.get("PROVIDES", None) 
145
146          # initialze the tag lists to empty
147          tpentry['tags'] = []
148          tpentry['mantags'] = []
149
150          # the tags are a comma-seperated list.
151          tags = attrs.get("TAGS", "").lower().strip().split(",")
152          for tag in tags:
153             # if a tag starts with a "+", then it's a mandatory tag
154             if tag[:1] == '+':
155                tpentry['mantags'].append(tag[1:])
156                tpentry['tags'].append(tag[1:])
157             else:
158                tpentry['tags'].append(tag)
159
160          self.tp_dict[self.user].append(tpentry)
161
162       elif tag == "USER":
163          # We will use username.publickey as our reference, so fetch it...
164          username = attrs.get("USERNAME");
165          publickey_string_raw = attrs.get("PUBLICKEY");
166          # publickey_string may either be an old mangled key string or a new
167          # valid key string, so get versions of it such that we know what they are
168          publickey_sl = arizonacrypt.fnstring_to_publickey_sl(publickey_string_raw)
169          publickey = arizonacrypt.PublicKey(sl=publickey_sl)
170
171          # when old code that only supports short keys (and always mangles keys) adds a public key
172          # for a long key user, it would have mangled even long keys. mangled long keys normally aren't
173          # detected by fnstring_to_publickey_sl's attempts at transparent backwards compatibility by
174          # detecting keys that should be unmangled by their length and unmangling them.
175          # to get around this, if we find the key we got back from fnstring_to_publickey_sl
176          # is invalid, try to get it again but this time forcibly unmangling. forcibly unmangling
177          # means that it will be unmangled despite the length of the key.
178          if not publickey.is_valid():
179              publickey_sl = arizonacrypt.fnstring_to_publickey_sl(publickey_string_raw, force_unmangling=True)
180              publickey = arizonacrypt.PublicKey(sl=publickey_sl)
181              if not publickey.is_valid():
182                  # jsamuel - would be good to display the name of the tpfile, but I can't see right now
183                  # where that is available
184                  raise ValueError, "When parsing tpfile, encountered invalid USER key: " + str(publickey_string_raw)
185
186          publickey_string = publickey.string
187          publickey_string_old = arizonacrypt.publickey_sl_to_fnstring_compat(publickey.sl)
188          publickey_hash = publickey.hash
189          longname = username + "." + publickey_string
190
191          if longname in self.parent_list:
192             # cycle detection -- print out an error messages
193             # only print the part of the filenames up to the "."
194             arizonareport.send_out(1, "  WARNING: cycle detected in tpfiles (" +
195                                       self.user.split(".")[0] +
196                                       " references " +
197                                       longname.split(".")[0] +
198                                       ")" )
199          else:
200             # Add the user action to the list...
201             tpentry = {}
202             tpentry['kind'] = "USER"
203             tpentry['pattern'] = attrs.get("PATTERN")
204             tpentry['tpfilename'] = longname
205             tpentry['action'] = attrs.get("ACTION", "unspecified").lower().strip()
206             tpentry['order-by'] = attrs.get("ORDER-BY", "default").lower().strip()
207
208             requiretags = attrs.get("REQUIRETAGS", "").lower().strip().split(",")
209             requiretags = [tag for tag in requiretags if tag != '']
210             tpentry['requiretags'] = requiretags
211
212             tpentry['provides'] = attrs.get("PROVIDES", None)
213
214             try:
215                self.include_tpfile(username, publickey)
216
217             except TPFileError, errMsg:
218                arizonareport.send_error(0, "Failed to include trusted package file " +
219                                               "user: " + username + ", keyhash: " + publickey_hash +
220                                               ", error: " + errMsg)
221
222                misstp = arizonaconfig.get_option("missingtp")
223                if misstp == "ignore":
224                   arizonareport.send_error(1, "Ignoring missing tpfile user: " + username)
225                elif misstp == "exit":
226                   arizonareport.send_error(0, "Halting due to missing tpfile user: " + username)
227                   sys.exit(1)
228                elif (misstp == "deny") and (tpentry['action'] == "allow"):
229                   # 'deny' differs from 'denyall' in the respect that if the
230                   # original rule is an 'allow', then deny turns it into a
231                   # no-op, so we can just ignore the rule
232                   tpentry = None
233                elif (misstp == "deny") or (misstp == "denyall"):
234                   # change the rule into a file deny rule
235                   tpentry['kind'] = "PACKAGE"
236                   tpentry['action'] = "deny"
237                   tpentry['tags'] = []
238                   tpentry['mantags'] = []
239
240             # add the rule to the current dictionary. If all went well, then
241             # this is the USER rule we built above. If something went wong,
242             # then it turned into a PACKAGE:DENY rule.
243             if tpentry:
244                self.tp_dict[self.user].append(tpentry)
245
246       else:
247          # Shouldn't happen if DTD is correct
248          raise arizonaxml.XMLError, "invalid element: " + tag
249
250
251
252
253
254    def handle_end_tag(self, tag):
255       """
256       <Purpose>
257          TODO fix comment
258       """
259       if tag == "TRUSTEDPACKAGES" or tag == "PACKAGE" or tag == "FILE" or tag == "USER":
260          return
261       else:
262          # Shouldn't happen if DTD is correct
263          raise arizonaxml.XMLError, "invalid closing element: " + tag
264
265
266
267 def PrintRepoHeader(dict, filename):
268     global glo_last_repo_printed
269     if glo_last_repo_printed != dict['name']:
270         glo_last_repo_printed = dict['name']
271         arizonareport.send_out(3, "Parsing tpfiles from repo: " + os.path.split(dict['name'])[0])
272         arizonareport.send_out(3, "  dir: " + os.path.dirname(filename))
273
274         storklog.info("repo: " + os.path.split(dict['name'])[0])
275         storklog.info("dir: " + os.path.dirname(filename))
276
277 def TrustedPackagesFileParse(dtd, filename, repo_dict, publickey_string, keyname, parents):
278    """
279    <Purpose>
280       Creates the parser for the trustedpackages file and parses it.
281       Returns a dictionary of group members.
282
283    <Arguments>
284       dtd:
285          XML dtd to use when parsing
286
287       filename:
288          filename of tpfile, including path and extension.
289
290       repo_dict:
291          repository whre filename came from; used to print message to client
292
293       publickey_string:
294          public key used to verify signature of tpfile named by filename.
295
296       keyname:
297          the name of the key where that this should be added under in the
298          returned dict.
299
300       parents:
301          a list build up of the keynames of the tpfiles that are parents of
302          this tpfile. used for cycle detection.
303
304    <Returns>
305       dictionary containing trusted packages entries.
306    """
307    if repo_dict:
308       PrintRepoHeader(repo_dict, filename)
309
310    arizonareport.send_out(3, "  tpfile: " + os.path.basename(filename))
311    storklog.info("tpfile: " + os.path.basename(filename))
312
313    publickey_sl = arizonacrypt.fnstring_to_publickey_sl(publickey_string)
314    temp_publickey_fn = arizonacrypt.sl_to_fn(publickey_sl)
315    try:
316       try:
317          if arizonacrypt.XML_timestamp_signedfile_with_publickey_fn(filename, temp_publickey_fn):
318             temp_contents = arizonacrypt.XML_retrieve_originalfile_from_signedfile(filename)
319       except TypeError, e:
320          arizonareport.send_error(0, str(e))
321          raise TPFileError, "verification error"
322
323    finally:
324       os.remove(temp_publickey_fn)
325
326    try:
327       app = __TrustedPackagesApplication(keyname, parents)
328       app = arizonaxml.XMLParse(app, dtd, temp_contents)
329       return app.tp_dict
330    finally:
331       os.remove(temp_contents)
332
333
334
335
336
337 def init():
338    """
339    <Purpose>
340       This function retrieves trustedpackages information from remote
341       repositories.
342
343    <Arguments>
344       None (uses command line options instead)
345
346    <Exceptions>
347       None
348
349    <Side Effects>
350       Updates trustedpackages files from remote repositories
351
352    <Returns>
353       None
354    """
355    
356    return
357
358    """ SMBAKER 2/28/2007
359       storkpackagelist.py will be responsible for dealing with repositories. 
360       we will assume storkrepolist.init()
361    """
362
363    if not storkrepolist.glo_initialized:
364       # the right thing to do may be to call storkpackagelist.init, but for now
365       # lets just assume we have a semantic error and the code needs fixing 
366       arizonareport.send_error(0, 
367           "ERROR: storktrustedpackageparse.init called before storkrepolist.init")
368       sys.exit(1)    
369
370
371
372
373
374
375
376
377
378
379
380
381
382