source: security/trunk/python/NDG/MyProxy.py @ 543

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/security/trunk/python/NDG/MyProxy.py@543
Revision 543, 24.3 KB checked in by pjkersha, 14 years ago (diff)

AttAuthority?.py, MyProxy?.py, Session.py: for all properties files, enable
file path properties to contain environment variables.

SimpleCA.py: setCAPassPhrase() now a property "caPassPhrase".

attAuthorityProperties.xml: modified Attribute Certificate life time
<attCertLifeTime> to 8 hours.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1"""NDG wrapper to MyProxy.  Also contains OpenSSLConfigFile class, a
2wrapper to the openssl configuration file.
3
4NERC Data Grid Project
5
6P J Kershaw 02/06/05
7
8Copyright (C) 2005 CCLRC & NERC
9
10This software may be distributed under the terms of the Q Public License,
11version 1.0 or later.
12"""
13
14cvsID = '$Id$'
15
16# Use pipes for stdin/stdout for MyProxy commands
17import os
18
19# Seeding for random number generator
20import time
21
22# Temporaries files created for MyProxy executables I/O
23import tempfile
24
25# Call MyProxy executables
26from subprocess import *
27
28import re
29
30# Optionally include X509 certificate reading for addUser method
31try:
32    from X509 import *
33except:
34    pass
35
36simpleCAdebug = False
37
38# SimpleCA may be called locally or via Web Service
39simpleCAImport = False
40try:
41    # Using SimpleCA service local to current machine
42    from SimpleCA import *
43    simpleCAImport = True
44    if simpleCAdebug:
45        print "NDG.SimpleCA loaded"
46   
47except ImportError, e:
48    if simpleCAdebug:
49        print "Skipping NDG.SimpleCA: %s" % e
50    pass
51
52try:
53    # Local SimpleCA not needed - may be using Web Service instead
54    from SimpleCAClient import *
55    simpleCAImport = True
56    if simpleCAdebug:
57        print "NDG.SimpleCAClient loaded"
58   
59except ImportError, e:
60    if simpleCAdebug:
61        print "Skipping NDG.SimpleCAClient: %s" % e
62    pass
63
64if not simpleCAImport:
65    raise ImportError(\
66        "Either SimpleCA or SimpleCAClient module must be present")
67
68
69# For parsing of properties file
70import cElementTree as ElementTree
71
72
73#_____________________________________________________________________________
74class MyProxyError(Exception):
75   
76    """Exception handling for NDG MyProxy class."""
77   
78    def __init__(self, msg):
79        self.__msg = msg
80         
81    def __str__(self):
82        return self.__msg
83
84
85
86
87#_____________________________________________________________________________
88class MyProxy:
89    """NDG wrapper to MyProxy server interface - use to serve proxy
90    certificate from user sign on"""
91
92    # valid configuration property keywords
93    __validKeys = ['myProxyServer',
94                   'gridSecurityDir',
95                   'credStorageDir',
96                   'openSSLConfFileName',
97                   'tmpDir',
98                   'path',
99                   'proxyCertMaxLifetime',
100                   'proxyCertLifetime',
101                   'simpleCACltProp',
102                   'simpleCASrvProp']
103
104   
105    def __init__(self, propFilePath=None, **prop):
106        """Initialise proxy certificate generation settings
107
108        propFilePath:   set properties via a configuration file
109        prop:           set properties via keywords"""
110
111
112        self.__prop = {}
113
114       
115        # Make a copy of the environment and then reset path restricting for
116        # use of MyProxy executables
117        #
118        # Use copy as direct assingment seems to take a reference to
119        # os.environ - if self.__env is changed, so is os.environ
120        self.__env = os.environ.copy()
121
122        # Configuration file used to get default subject when generating a
123        # new certificate
124        self.__openSSLConf = OpenSSLConfigFile()
125
126       
127        # Properties set via input keywords
128        self.setProperties(**prop)
129
130        # If properties file is set any parameters settings in file will
131        # override those set by input keyword
132        if propFilePath is not None:
133            self.readProperties(propFilePath)
134       
135
136        # Grid security directory - environment setting overrides
137        if 'GRID_SECURITY_DIR' in self.__env:
138            self.__prop['gridSecurityDir'] = self.__env['GRID_SECURITY_DIR']           
139
140            openSSLConfFilePath = os.path.join(self.__prop['gridSecurityDir'],
141                                            self.__prop['openSSLConfFileName'])
142           
143            self.__openSSLConf.setFilePath(openSSLConfFilePath)
144
145       
146        if os.environ['GLOBUS_LOCATION'] is None:
147            raise MyProxyError(\
148                        "Environment variable \"GLOBUS_LOCATION\" is not set")
149
150
151        # Server host name - environment setting overrides
152        if 'MYPROXY_SERVER' in self.__env:
153            self.__prop['myProxyServer'] = self.__env['MYPROXY_SERVER'] 
154
155
156        # Executables - for getDelegation: 
157        self.__getDelegExe = "myproxy-get-delegation"
158
159        # ... and for addUser:
160        self.__gridCertReqExe = 'openssl'
161        self.__adminLoadCredExe = 'myproxy-admin-load-credential'
162        self.__userIsRegExe = 'myproxy-admin-query'
163           
164
165
166
167    def setProperties(self, **prop):
168        """Update existing properties from an input dictionary
169        Check input keys are valid names"""
170       
171        for key in prop.keys():
172            if key not in self.__validKeys:
173                raise MyProxyError("Property name \"%s\" is invalid" % key)
174               
175        self.__prop.update(prop)
176
177
178        # Update path
179        if 'path' in prop:
180            self.__env['PATH'] = self.__prop['path']
181
182        # Update openssl conf file path
183        if 'gridSecurityDir' in prop or 'openSSLConfFileName' in prop:
184           
185            openSSLConfFilePath = os.path.join(self.__prop['gridSecurityDir'],
186                                            self.__prop['openSSLConfFileName'])
187           
188            self.__openSSLConf.setFilePath(openSSLConfFilePath)
189
190
191
192
193    def readProperties(self, propFilePath=None, propElem=None):
194        """Read XML properties from a file or cElementTree node
195       
196        propFilePath|propertiesElem
197
198        propFilePath: set to read from the specified file
199        propertiesElem:     set to read beginning from a cElementTree node"""
200
201        if propFilePath is not None:
202
203            try:
204                tree = ElementTree.parse(propFilePath)
205                propElem = tree.getroot()
206               
207            except IOError, e:
208                raise MyProxyError(\
209                                "Error parsing properties file \"%s\": %s" % \
210                                (e.filename, e.strerror))
211
212               
213            except Exception, e:
214                raise MyProxyError("Error parsing properties file: %s" % \
215                                    str(e))
216
217        if propElem is None:
218            raise MyProxyError("Root element for parsing is not defined")
219
220
221        # Get properties as a data dictionary
222        prop = {}
223        for elem in propElem:
224
225            # Check for environment variables in file paths
226            tagCaps = elem.tag.upper()
227            if 'FILE' in tagCaps or 'PATH' in tagCaps or 'DIR' in tagCaps:
228                elem.text = os.path.expandvars(elem.text)
229
230            prop[elem.tag] = elem.text
231           
232
233        # Check for SimpleCA properties - should be either WS client or
234        # local server property settings
235        if 'simpleCACltProp' in prop:
236
237            tagElem = propElem.find('simpleCACltProp')
238            if not tagElem:
239                raise MyProxyError("Tag %s not found in file" % \
240                                   'simpleCACltProp')
241           
242            try:
243                simpleCAClt = SimpleCAClient()
244                simpleCAClt.readProperties(propElem=tagElem)
245               
246            except Exception, e:
247                raise MyProxyError("Setting SimpleCAClient properties: %s"%e)
248
249            prop['simpleCACltProp'] = simpleCAClt()
250           
251        elif 'simpleCASrvProp' in prop:
252
253            tagElem = propElem.find('simpleCASrvProp')
254            if not tagElem:
255                raise MyProxyError("Tag %s not found in file" % \
256                                   'simpleCASrvProp')
257           
258            try:
259                simpleCA = SimpleCA()
260                simpleCA.readProperties(propElem=tagElem)
261               
262            except Exception, e:
263                raise MyProxyError("Setting SimpleCA properties: %s" % e)
264
265            prop['simpleCASrvProp'] = simpleCA()
266
267        else:
268            raise MyProxyError(\
269                "Neither %s or %s tags found in properties file" % \
270                ('simpleCACltProp', 'simpleCASrvProp'))
271
272
273        self.setProperties(**prop)
274
275
276
277       
278    def getDelegation(self, userName, passPhrase):
279        """Generate a proxy certificate given the MyProxy username and
280        passphrase"""
281
282        errMsgTmpl = "Getting delegation for %s: %s"
283
284           
285        # Call proxy request command
286        try:
287            try:
288                # Create a temporary to hold the proxy certificate file output
289                proxyCertFile = tempfile.NamedTemporaryFile()
290
291               
292                # Set up command + arguments
293                #
294                # TODO: -s <hostname> arg needed? - MYPROXY_SERVER environment
295                # variable is set via __env
296                #
297                # P J Kershaw 27/06/05
298                getDelegCmd = [self.__getDelegExe,
299                               '-S',
300                               '-s', self.__prop['myProxyServer'],
301                               '-l', userName,
302                               '-t', str(self.__prop['proxyCertLifetime']),
303                               '-o', proxyCertFile.name]
304
305                # Open a pipe to send the pass phrase through stdin - avoid
306                # exposing pass phrase as a command line arg for security
307                getDelegPipeR, getDelegPipeW = os.pipe()
308                os.write(getDelegPipeW, passPhrase)
309               
310                getDelegProc = Popen(getDelegCmd,
311                                     stdin=getDelegPipeR,
312                                     stdout=PIPE,
313                                     stderr=PIPE,
314                                     close_fds=True,
315                                     env=self.__env)               
316            finally:
317                try:
318                    os.close(getDelegPipeR)
319                    os.close(getDelegPipeW)
320                except: pass
321
322
323            # File must be closed + close_fds set to True above otherwise
324            # wait() call will hang               
325            if getDelegProc.wait():
326                errMsg = getDelegProc.stderr.read()
327                raise MyProxyError(errMsg)
328
329            # Get certificate created
330            sProxyCert = open(proxyCertFile.name).read()
331               
332        except IOError, e:               
333            raise MyProxyError(errMsgTmpl % (userName, e.strerror))
334       
335        except OSError, e:
336            raise MyProxyError(errMsgTmpl % (userName, e.strerror))
337       
338        except Exception, e:
339            raise MyProxyError(errMsgTmpl % (userName, str(e)))
340
341
342        return sProxyCert
343
344
345
346   
347    def addUser(self,
348                userName,
349                userPassPhrase,
350                cn=None,
351                retDN=False,
352                caPassPhrase=None,
353                caConfigFilePath=None,
354                **prop):
355       
356        """Add a new user generating a new certificate and adding it to the
357        MyProxy repository
358
359        userName:                       user name or new user - must be unique
360                                        to the repository                               
361        userPassPhrase:                 pass phrase to be used with the user
362                                        name
363        cn:                             Common name to be used on the
364                                        certificate to be generated
365                               
366        retDN:                          if set to True, return the
367                                        Distinguished Name for the new user.
368                                        By default, a dictionary is returned
369                                        containing a key, 'keyFile' set to the
370                                        text of the private key generated for
371                                        the new user.  If retDN is set too,
372                                        then an additional key 'dn' will be
373                                        included in the dictionary, set to the
374                                        DN for the new user.
375
376        caConfigFilePath|caPassPhrase:  pass phrase for SimpleCA's
377                                        certificate.  Set via file or direct
378                                        string input respectively.  Set here
379                                        to override setting [if any] made at
380                                        object creation.
381       
382                                        Passphrase is only required if
383                                        SimpleCA is instantiated on the local
384                                        machine.  If SimpleCA WS is called no
385                                        passphrase is required.
386                               
387        **prop:                         keywords corresponding to
388                                        configuration properties normally set
389                                        in properties file.  Set here to
390                                        override.
391        """
392
393       
394        # Default Common name to the username
395        if cn is None: cn = userName
396
397           
398        # Check user name doesn't already exist
399        if self.userIsRegistered(userName):
400            raise MyProxyError("Username '%s' already exists" % userName)
401
402
403        self.setProperties(**prop)
404
405
406        # addUSer returns a dictionary containing the key 'keyFile' which is
407        # the text of the private key generated by the certificate request
408        # but also, if the retDN input flag is set to True, a key 'dn' set to
409        # the distinguished name of the new user
410        user = {}
411
412
413        # Error message prefix for certificate request call
414        errMsgTmpl = "Certificate request for new user '%s': %s"
415
416
417        # Create certificate request and key files as temporary files.  Once
418        # the new certificate has been uploaded to the proxy server they may
419        # be discarded.
420        #
421        # Using NamedTemporaryFile, they are deleted when the temp file
422        # objects go out of scope
423        keyFile = tempfile.NamedTemporaryFile('w', -1, '.pem', 'key-',
424                                              self.__prop['tmpDir'])
425        certReqFile = tempfile.NamedTemporaryFile('w', -1, '.pem', 'certReq-',
426                                                  self.__prop['tmpDir'])
427
428
429        # Read default DN parameters from the Globud Open SSL configuration
430        # file
431        reqDN = self.__openSSLConf.getReqDN()
432
433        # Make into a string adding in the Common Name
434        reqDnTxt = \
435"""%(0.organizationName)s
436%(0.organizationalUnitName)s
437""" % reqDN + cn + os.linesep
438
439
440        try:
441            try:
442                # Open a pipe to send the required DN text in via stdin
443                reqDnR, reqDnW = os.pipe()
444                os.write(reqDnW, reqDnTxt)
445
446                # Create a temporary file
447                addUserTmp = tempfile.NamedTemporaryFile()
448                open(addUserTmp.name, 'w').write(userPassPhrase)
449
450                # Files to seed random number generation - this is a loose
451                # copy of what grid-cert-request shell script does
452                randFile = self.__mkRandTmpFile()
453                randFileList = randFile.name + \
454                               ":/var/adm/wtmp:/var/log/messages"
455               
456                # Using openssl command rather than grid-cert-request wrapper
457                # as latter doesn't include the command line options needed
458                gridCertReqCmd = [self.__gridCertReqExe,
459                                  "req",
460                                  "-new",
461                                  "-keyout", keyFile.name,
462                                  "-out", certReqFile.name,
463                                  "-passout", "file:" + addUserTmp.name,
464                                  "-config", self.__openSSLConf.getFilePath(),
465                                  "-rand", randFileList]
466               
467                gridCertReqProc = Popen(gridCertReqCmd,
468                                        stdin=reqDnR,
469                                        stdout=PIPE,
470                                        stderr=STDOUT,
471                                        close_fds=True,
472                                        env=self.__env)
473               
474                if gridCertReqProc.wait():
475                    errMsg = gridCertReqProc.stdout.read()
476                    raise MyProxyError(errMsg)
477
478            finally:
479                try:
480                    os.close(reqDnR)
481                    os.close(reqDnW)
482
483                    # Read key file into string buffer to be returned
484                    user['keyFile'] = open(keyFile.name).read()
485                   
486                    # Closing temporary files deletes them.
487                    addUserTmp.close()
488                    randFile.close()
489                except: pass
490               
491        except IOError, e:               
492            raise MyProxyError(errMsgTmpl % (userName, e.strerror))
493       
494        except OSError, e:
495            raise MyProxyError(errMsgTmpl % (userName, e.strerror))
496       
497        except Exception, e:
498            raise MyProxyError(errMsgTmpl % (userName, e))
499
500       
501        # Get the SimpleCA to sign the request - call locally or via WS
502        # depending on which properties were set
503        # WS call has precedence
504        if 'simpleCACltProp' in self.__prop: 
505
506            # Client properties were set - initiate client to SimpleCA web
507            # service
508            try:
509                simpleCAClt = SimpleCAClient(**self.__prop['simpleCACltProp'])
510                sCert = simpleCAClt.reqCert(certReqFilePath=certReqFile.name)
511               
512            except Exception, e:
513                raise MyProxyError("Calling SimpleCA WS for user '%s': %s" % \
514                                   (userName, e))
515           
516        elif 'simpleCASrvProp' in self.__prop:
517           
518            # Server properties were set - Create local instance SimpleCA
519            # server
520            try:
521                simpleCA = SimpleCA(**self.__prop['simpleCASrvProp'])
522                sCert = simpleCA.sign(certReqFilePath=certReqFile.name,
523                                      configFilePath=caConfigFilePath,
524                                      caPassPhrase=caPassPhrase)
525               
526            except Exception, e:
527                raise MyProxyError("Calling SimpleCA for user '%s': %s" % \
528                                   (userName, e))
529        else:
530            raise MyProxyError(\
531                "Either Simple CA WS client or Simple CA server must be set")
532
533
534        # Copy new certificate into temporary file ready for call to load
535        # credential
536        certFile = tempfile.NamedTemporaryFile('r', -1, '.pem', 'cert-',
537                                               self.__prop['tmpDir'])
538        try:
539            open(certFile.name, "w").write(sCert)
540           
541        except Exception, e:
542            raise MyProxyError(\
543                        "Writing certificate temporary file \"%s\": %s" % \
544                        (certFile.name, e))
545       
546        # Upload to MyProxy
547        errMsgTmpl = "Uploading certificate to MyProxy for new user '%s': %s"
548
549        adminLoadCredCmd = [self.__adminLoadCredExe,
550                            '-l', userName,
551                            '-c', certFile.name,
552                            '-y', keyFile.name,
553                            '-t', str(self.__prop['proxyCertMaxLifetime']),
554                            '-s', self.__prop['credStorageDir']]
555
556        try:
557            try:
558                adminLoadCredProc = Popen(adminLoadCredCmd,
559                                          stdout=PIPE,
560                                          stderr=STDOUT,
561                                          env=self.__env)
562               
563                if adminLoadCredProc.wait():
564                    errMsg = adminLoadCredProc.stdout.read()
565                    raise MyProxyError(errMsg)
566            finally:
567                try:
568                    keyFile.close()
569                except:
570                    pass
571               
572        except IOError, e:               
573            raise MyProxyError(errMsgTmpl % (userName, e.strerror))
574       
575        except OSError, e:
576            raise MyProxyError(errMsgTmpl % (userName, e.strerror))
577       
578        except Exception, e:
579            raise MyProxyError(errMsgTmpl % (userName, e))
580
581
582        if retDN:
583            try:
584                # Add an additional key to the dictionary output containing
585                # the new user's DN
586                user['dn'] = X509CertRead(certFile.name).dn.serialise()
587               
588            except Exception, e:
589                raise MyProxyError(\
590                    "Error returning DN for new certificate for user: " + \
591                    userName)
592
593        return user
594
595   
596           
597
598    def userIsRegistered(self, userName):
599        """Return True if given username is registered in the repository"""
600       
601        errMsgTmpl = "Checking for user '%s': %s"
602        userIsRegCmd = [self.__userIsRegExe,
603                        '-l', userName,
604                        '-s', self.__prop['credStorageDir']]
605
606        try:
607            userIsRegProc = Popen(userIsRegCmd, stdout=PIPE, stderr=STDOUT)
608           
609            if userIsRegProc.wait():
610                errMsg = userIsRegProc.stdout.read()
611                raise MyProxyError(errMsg)
612           
613            # Search for text matching expected output for username found
614            # Exit status from command seems to be 0 regardless of whether the
615            # username is found or not
616            outMsg = userIsRegProc.stdout.read()
617            if outMsg.find("username: " + userName) != -1:
618                return True
619            else:
620                return False
621                                       
622        except IOError, e:               
623            raise MyProxyError(errMsgTmpl % (userName, e.strerror))
624       
625        except OSError, e:
626            raise MyProxyError(errMsgTmpl % (userName, e.strerror))
627       
628        except Exception, e:
629            raise MyProxyError(errMsgTmpl % (userName, e))
630
631
632
633
634    def __mkRandTmpFile(self):
635        """Make a file containing random data to seed the random number
636        generator used for the certificate request generation in addUser"""
637
638        randomTmpFile = tempfile.NamedTemporaryFile()
639
640        f = open(randomTmpFile.name, 'w')
641        f.write(os.urandom(1000))
642        f.write(time.asctime())
643        f.write(''.join(os.listdir(tempfile.tempdir)))
644        f.write(''.join(os.listdir(os.environ['HOME'])))
645        f.close()
646       
647        return randomTmpFile
648   
649
650
651   
652#_____________________________________________________________________________       
653class OpenSSLConfigFile:
654    """NDG Wrapper to OpenSSL Configuration file"""
655   
656    __reqDnRE = '\[ req_distinguished_name \].*\['
657   
658    def __init__(self, filePath=None):
659
660        self.setFilePath(filePath)
661
662           
663    def setFilePath(self, filePath=None):
664        """Set file path for OpenSSL configuration file"""
665        if filePath is not None:
666            if not isinstance(filePath, basestring):
667                raise MyProxyError(\
668                    "Input Grid Certificate file path must be a string")
669
670            self.__filePath = filePath
671                   
672            try:
673                if not os.access(self.__filePath, os.R_OK):
674                    raise MyProxyError("not found or no read access")
675                                         
676            except Exception, e:
677                raise MyProxyError(\
678                    "Grid Certificate file path is not valid: \"%s\": %s" % \
679                    (self.__filePath, str(e)))
680
681
682    def getFilePath(self):
683        """Get file path for OpenSSL configuration file"""
684        return self.__filePath
685
686
687    def read(self):
688        """Read OpenSSL configuration file and return as string"""
689
690        file = open(self.__filePath, 'r')
691        fileTxt = file.read()
692        file.close()
693       
694        return fileTxt
695
696
697    def getReqDN(self):
698        """Read Required DN parameters from the configuration file returning
699        them in a dictionary"""
700       
701        # Nb. Match over line boundaries
702        reqDnTxt = re.findall(self.__reqDnRE, self.read(), re.S)[0]
703
704        # Separate lines
705        reqDnLines = reqDnTxt.split(os.linesep)
706       
707        # Match the '*_default' entries and make a dictionary
708        #
709        # Make sure comment lies are omitted - P J Kershaw 22/07/05
710        return dict([re.split('_default\s*=\s*', line) for line in reqDnLines\
711                     if re.match('[^#].*_default\s*=', line)]) 
Note: See TracBrowser for help on using the repository browser.