source: TI12-security/trunk/python/ndg.security.server/ndg/security/server/MyProxy.py @ 1773

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.server/ndg/security/server/MyProxy.py@1785
Revision 1773, 26.1 KB checked in by pjkersha, 13 years ago (diff)

Integration of Session Manager client server and unit test packages. Tested with simple getX509Cert
call to Session Manager web service.

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