]> git.decadent.org.uk Git - dak.git/blob - dak/import_keyring.py
Merge commit 'ftpmaster/master' into sqlalchemy
[dak.git] / dak / import_keyring.py
1 #!/usr/bin/env python
2
3 """ Imports a keyring into the database """
4 # Copyright (C) 2007  Anthony Towns <aj@erisian.com.au>
5
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20 ################################################################################
21
22 import sys, os, re
23 import apt_pkg, ldap, email.Utils
24
25 from daklib.config import Config
26 from daklib.dbconn import *
27 from daklib import utils
28
29
30 # Globals
31 Options = None
32
33 ################################################################################
34
35 def get_uid_info(session):
36     byname = {}
37     byid = {}
38     q = session.execute("SELECT id, uid, name FROM uid")
39     for (keyid, uid, name) in q.fetchall():
40         byname[uid] = (keyid, name)
41         byid[keyid] = (uid, name)
42     return (byname, byid)
43
44 def get_fingerprint_info(session):
45     fins = {}
46     q = session.execute("SELECT f.fingerprint, f.id, f.uid, f.keyring FROM fingerprint f")
47     for (fingerprint, fingerprint_id, uid, keyring) in q.fetchall():
48         fins[fingerprint] = (uid, fingerprint_id, keyring)
49     return fins
50
51 ################################################################################
52
53 def get_ldap_name(entry):
54     name = []
55     for k in ["cn", "mn", "sn"]:
56         ret = entry.get(k)
57         if ret and ret[0] != "" and ret[0] != "-":
58             name.append(ret[0])
59     return " ".join(name)
60
61 ################################################################################
62
63 class Keyring(object):
64     gpg_invocation = "gpg --no-default-keyring --keyring %s" +\
65                      " --with-colons --fingerprint --fingerprint"
66     keys = {}
67     fpr_lookup = {}
68
69     def de_escape_gpg_str(self, str):
70         esclist = re.split(r'(\\x..)', str)
71         for x in range(1,len(esclist),2):
72             esclist[x] = "%c" % (int(esclist[x][2:],16))
73         return "".join(esclist)
74
75     def __init__(self, keyring):
76         self.cnf = Config()
77         k = os.popen(self.gpg_invocation % keyring, "r")
78         keys = self.keys
79         key = None
80         fpr_lookup = self.fpr_lookup
81         signingkey = False
82         for line in k.xreadlines():
83             field = line.split(":")
84             if field[0] == "pub":
85                 key = field[4]
86                 (name, addr) = email.Utils.parseaddr(field[9])
87                 name = re.sub(r"\s*[(].*[)]", "", name)
88                 if name == "" or addr == "" or "@" not in addr:
89                     name = field[9]
90                     addr = "invalid-uid"
91                 name = self.de_escape_gpg_str(name)
92                 keys[key] = {"email": addr}
93                 if name != "": keys[key]["name"] = name
94                 keys[key]["aliases"] = [name]
95                 keys[key]["fingerprints"] = []
96                 signingkey = True
97             elif key and field[0] == "sub" and len(field) >= 12:
98                 signingkey = ("s" in field[11])
99             elif key and field[0] == "uid":
100                 (name, addr) = email.Utils.parseaddr(field[9])
101                 if name and name not in keys[key]["aliases"]:
102                     keys[key]["aliases"].append(name)
103             elif signingkey and field[0] == "fpr":
104                 keys[key]["fingerprints"].append(field[9])
105                 fpr_lookup[field[9]] = key
106
107     def generate_desired_users(self):
108         if Options["Generate-Users"]:
109             format = Options["Generate-Users"]
110             return self.generate_users_from_keyring(format)
111         if Options["Import-Ldap-Users"]:
112             return self.import_users_from_ldap()
113         return ({}, {})
114
115     def import_users_from_ldap(self):
116         LDAPDn = self.cnf["Import-LDAP-Fingerprints::LDAPDn"]
117         LDAPServer = self.cnf["Import-LDAP-Fingerprints::LDAPServer"]
118         l = ldap.open(LDAPServer)
119         l.simple_bind_s("","")
120         Attrs = l.search_s(LDAPDn, ldap.SCOPE_ONELEVEL,
121                "(&(keyfingerprint=*)(gidnumber=%s))" % (self.cnf["Import-Users-From-Passwd::ValidGID"]),
122                ["uid", "keyfingerprint", "cn", "mn", "sn"])
123
124         ldap_fin_uid_id = {}
125
126         byuid = {}
127         byname = {}
128         keys = self.keys
129         fpr_lookup = self.fpr_lookup
130
131         for i in Attrs:
132             entry = i[1]
133             uid = entry["uid"][0]
134             name = get_ldap_name(entry)
135             fingerprints = entry["keyFingerPrint"]
136             keyid = None
137             for f in fingerprints:
138                 key = fpr_lookup.get(f, None)
139                 if key not in keys: continue
140                 keys[key]["uid"] = uid
141
142                 if keyid != None: continue
143                 keyid = database.get_or_set_uid_id(uid)
144                 byuid[keyid] = (uid, name)
145                 byname[uid] = (keyid, name)
146
147         return (byname, byuid)
148
149     def generate_users_from_keyring(self, format):
150         byuid = {}
151         byname = {}
152         keys = self.keys
153         any_invalid = False
154         for x in keys.keys():
155             if keys[x]["email"] == "invalid-uid":
156                 any_invalid = True
157                 keys[x]["uid"] = format % "invalid-uid"
158             else:
159                 uid = format % keys[x]["email"]
160                 keyid = database.get_or_set_uid_id(uid)
161                 byuid[keyid] = (uid, keys[x]["name"])
162                 byname[uid] = (keyid, keys[x]["name"])
163                 keys[x]["uid"] = uid
164         if any_invalid:
165             uid = format % "invalid-uid"
166             keyid = database.get_or_set_uid_id(uid)
167             byuid[keyid] = (uid, "ungeneratable user id")
168             byname[uid] = (keyid, "ungeneratable user id")
169         return (byname, byuid)
170
171 ################################################################################
172
173 def usage (exit_code=0):
174     print """Usage: dak import-keyring [OPTION]... [KEYRING]
175   -h, --help                  show this help and exit.
176   -L, --import-ldap-users     generate uid entries for keyring from LDAP
177   -U, --generate-users FMT    generate uid entries from keyring as FMT"""
178     sys.exit(exit_code)
179
180
181 ################################################################################
182
183 def main():
184     global Options
185
186     cnf = Config()
187     Arguments = [('h',"help","Import-Keyring::Options::Help"),
188                  ('L',"import-ldap-users","Import-Keyring::Options::Import-Ldap-Users"),
189                  ('U',"generate-users","Import-Keyring::Options::Generate-Users", "HasArg"),
190                 ]
191
192     for i in [ "help", "report-changes", "generate-users", "import-ldap-users" ]:
193         if not cnf.has_key("Import-Keyring::Options::%s" % (i)):
194             cnf["Import-Keyring::Options::%s" % (i)] = ""
195
196     keyring_names = apt_pkg.ParseCommandLine(cnf.Cnf, Arguments, sys.argv)
197
198     ### Parse options
199
200     Options = cnf.SubTree("Import-Keyring::Options")
201     if Options["Help"]:
202         usage()
203
204     if len(keyring_names) != 1:
205         usage(1)
206
207     ### Keep track of changes made
208
209     changes = []   # (uid, changes strings)
210
211     ### Initialise
212     session = DBConn().session()
213
214     ### Cache all the existing fingerprint entries
215     db_fin_info = get_fingerprint_info(session)
216
217     ### Parse the keyring
218
219     keyringname = keyring_names[0]
220     keyring = Keyring(keyringname)
221
222     is_dm = "false"
223     if cnf.has_key("Import-Keyring::"+keyringname+"::Debian-Maintainer"):
224         session.execute("UPDATE keyrings SET debian_maintainer = :dm WHERE name = :name",
225                         {'dm': cnf["Import-Keyring::"+keyringname+"::Debian-Maintainer"],
226                          'name': keyringname.split("/")[-1]})
227
228         is_dm = cnf["Import-Keyring::"+keyringname+"::Debian-Maintainer"]
229
230     keyring_id = database.get_or_set_keyring_id(
231                         keyringname.split("/")[-1])
232
233     ### Generate new uid entries if they're needed (from LDAP or the keyring)
234     (desuid_byname, desuid_byid) = keyring.generate_desired_users()
235
236     ### Cache all the existing uid entries
237     (db_uid_byname, db_uid_byid) = get_uid_info(session)
238
239     ### Update full names of applicable users
240     for keyid in desuid_byid.keys():
241         uid = (keyid, desuid_byid[keyid][0])
242         name = desuid_byid[keyid][1]
243         oname = db_uid_byid[keyid][1]
244         if name and oname != name:
245             changes.append((uid[1], "Full name: %s" % (name)))
246             session.execute("UPDATE uid SET name = :name WHERE id = :keyid",
247                             {'name': name, 'keyid': keyid})
248
249     # The fingerprint table (fpr) points to a uid and a keyring.
250     #   If the uid is being decided here (ldap/generate) we set it to it.
251     #   Otherwise, if the fingerprint table already has a uid (which we've
252     #     cached earlier), we preserve it.
253     #   Otherwise we leave it as None
254
255     fpr = {}
256     for z in keyring.keys.keys():
257         keyid = db_uid_byname.get(keyring.keys[z].get("uid", None), [None])[0]
258         if keyid == None:
259             keyid = db_fin_info.get(keyring.keys[z]["fingerprints"][0], [None])[0]
260         for y in keyring.keys[z]["fingerprints"]:
261             fpr[y] = (keyid,keyring_id)
262
263     # For any keys that used to be in this keyring, disassociate them.
264     # We don't change the uid, leaving that for historical info; if
265     # the id should change, it'll be set when importing another keyring.
266
267     for f,(u,fid,kr) in db_fin_info.iteritems():
268         if kr != keyring_id: continue
269         if f in fpr: continue
270         changes.append((db_uid_byid.get(u, [None])[0], "Removed key: %s" % (f)))
271         session.execute("UPDATE fingerprint SET keyring = NULL WHERE id = :fprid", {'fprid': fid})
272
273     # For the keys in this keyring, add/update any fingerprints that've
274     # changed.
275
276     for f in fpr:
277         newuid = fpr[f][0]
278         newuiduid = db_uid_byid.get(newuid, [None])[0]
279         (olduid, oldfid, oldkid) = db_fin_info.get(f, [-1,-1,-1])
280         if olduid == None: olduid = -1
281         if oldkid == None: oldkid = -1
282         if oldfid == -1:
283             changes.append((newuiduid, "Added key: %s" % (f)))
284             if newuid:
285                 session.execute("""INSERT INTO fingerprint (fingerprint, uid, keyring)
286                                         VALUES (:fpr, :uid, :keyring)""",
287                                 {'fpr': f, 'uid': uid, 'keyring': keyring_id})
288             else:
289                 session.execute("""INSERT INTO fingerprint (fingerprint, keyring)
290                                         VALUES (:fpr, :keyring)""",
291                                 {'fpr': f, 'keyring': keyring_id})
292         else:
293             if newuid and olduid != newuid:
294                 if olduid != -1:
295                     changes.append((newuiduid, "Linked key: %s" % f))
296                     changes.append((newuiduid, "  (formerly belonging to %s)" % (db_uid_byid[olduid][0])))
297                 else:
298                     changes.append((newuiduid, "Linked key: %s" % f))
299                     changes.append((newuiduid, "  (formerly unowned)"))
300                 session.execute("UPDATE fingerprint SET uid = :uid WHERE id = :fpr",
301                                 {'uid': newuid, 'fpr': oldfid})
302
303             if oldkid != keyring_id:
304                 # Only change the keyring if it won't result in a loss of permissions
305                 q = session.execute("SELECT debian_maintainer FROM keyrings WHERE id = :keyring",
306                                     {'keyring': keyring_id})
307                 if is_dm == "false" and not q.fetchall()[0][0]:
308                     session.execute("UPDATE fingerprint SET keyring = :keyring WHERE id = :fpr",
309                                     {'keyring': keyring_id, 'fpr': oldfid})
310                 else:
311                     print "Key %s exists in both DM and DD keyrings. Not demoting." % (f)
312
313     # All done!
314     session.commit()
315
316     changesd = {}
317     for (k, v) in changes:
318         if k not in changesd: changesd[k] = ""
319         changesd[k] += "    %s\n" % (v)
320
321     keys = changesd.keys()
322     keys.sort()
323     for k in keys:
324         print "%s\n%s\n" % (k, changesd[k])
325
326 ################################################################################
327
328 if __name__ == '__main__':
329     main()