root/trunk/rp/trac/infocard_acct/0.11/infocard_acct/web_ui.py

Revision 1509, 17.6 kB (checked in by dbuss, 2 years ago)

change handling of token to only report errors if the transport wasn't encrypted.

  • Property svn:eol-style set to native
Line 
1#  Copyright (c) 2007 Novell, Inc.
2#  All Rights Reserved.
3 
4#  This library is free software; you can redistribute it and/or
5#  modify it under the terms of the GNU Lesser General Public License as
6#  published by the Free Software Foundation; version 2.1 of the license.
7 
8#  This library is distributed in the hope that it will be useful,
9#  but WITHOUT ANY WARRANTY; without even the implied warranty of
10#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11#  GNU Lesser General Public License for more details.
12 
13#  You should have received a copy of the GNU Lesser General Public License
14#  along with this library; if not, contact Novell, Inc.
15 
16#  To contact Novell about this file by physical or electronic mail,
17#  you may find current contact information at www.novell.com
18#
19#  This file incorporates work covered by the following copyright and 
20#   permission notice: 
21#
22#     -*- coding: utf8 -*-
23#
24#     Copyright (C) 2007 Matthew Good <trac@matt-good.net>
25#
26#     "THE BEER-WARE LICENSE" (Revision 42):
27#     <trac@matt-good.net> wrote this file.  As long as you retain this notice you
28#     can do whatever you want with this stuff. If we meet some day, and you think
29#     this stuff is worth it, you can buy me a beer in return.   Matthew Good
30#
31#     Author: Matthew Good <trac@matt-good.net>
32
33import random
34import string
35import re
36import time
37import urlparse
38
39from trac import perm, util
40from trac.core import *
41from trac.config import BoolOption, IntOption, Option, PathOption
42from trac.notification import NotificationSystem, NotifyEmail
43from trac.prefs import IPreferencePanelProvider
44from trac.web import auth
45from trac.web.api import IAuthenticator
46from trac.web.main import IRequestHandler
47from trac.web.chrome import INavigationContributor, ITemplateProvider
48from trac.util import Markup, hex_entropy
49
50from acct_mgr.api import AccountManager
51from acct_mgr import web_ui
52from acct_mgr.web_ui import AccountModule
53
54from association import AssociationManager
55from session import SecTokenSessionModule
56
57import infocard.xmlseclibs, infocard.infocardlib
58from infocard import infocardlib, event
59from infocard.infocardlib import SecToken, InfoCardProcessor
60
61def if_enabled(func):
62    def wrap(self, *args, **kwds):
63        if not self.enabled:
64            return None
65        return func(self, *args, **kwds)
66    return wrap
67
68class LoginModule(auth.LoginModule):
69
70    implements(ITemplateProvider)
71
72    privateKey = '/'
73    privateKeyPassPhrase = ''
74    mandatoryClaims = ('http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier',)
75
76    def __init__(self):
77        self.processors = {}
78
79        self.privateKey = self.config.get('infocard_acct', 'private_key_path')
80        self.privateKeyPassPhrase = self.config.get('infocard_acct', 'private_key_pass_phrase')
81        self.processors['infocard_acct'] = self._process_options('infocard_acct')
82       
83        #read any sets of infocard options
84        Sets =  self.config.getlist('infocard_acct', 'infocard_definitions')
85        if Sets:
86            for element in Sets:
87                name = element.strip()  #remove any unsightly spaces
88                self.processors[name] = self._process_options(name)
89       
90        scheme, host = urlparse.urlparse(self.env.abs_href())[:2]
91        if not scheme or not host:
92            self.log.error('Trac configuration for \'base_url\' option '\
93            'doesn\'t contain host and scheme information.')
94           
95    def _process_options(self, tag):
96        """read a block of options from the configuration and store them in a
97        local structure for later use.
98        """
99        options = {}
100        #uniquely name this block of options using it's tag from trac.ini
101        options['blockID'] = tag
102
103        #process basic options with no defaults
104        config_fields = ['required_claims', 'optional_claims',
105            'privacy_url', 'privacy_version', 'issuer', 'associated_user',
106            'Audience']
107        for field in config_fields:
108            data =  self.config.get(tag, field)
109            if data:
110                options[field] = data
111
112        #process warning levels and debug options, these may result in upgrades
113        #or downgrades in error reporting
114        for field in event.config_fields:
115            data = self.config.get(tag, field)
116            if data:
117                options[field] = data
118
119        #process options which have defaults and need to be
120        options['debug_page'] = self.config.getbool(tag, 'debug_page', False)
121        options['header_text'] = self.config.get(tag, 'header_text', 'InfoCard Login')
122        options['help_text'] = self.config.get(tag, 'help_text', 'Use any InfoCard to login to your existing account')
123        options['token_type'] = self.config.get(tag, 'token_type', 'http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1')
124
125        #append all hard coded mandatory claims to the required claims
126        if options.has_key('required_claims'):
127            options['required_claims'] = options['required_claims'] + " " + " ".join(self.mandatoryClaims)
128        else:
129            options['required_claims'] = " " + ' '.join(self.mandatoryClaims)
130
131        Audience = self.env.abs_href().replace('http:', '(http|https):', 1)
132        options['Audience'] = Audience + '/login/?$'
133         
134        #setup basic infocard processor for duty
135        processor = InfoCardProcessor()
136        if processor:
137            processor.setDecode(self.privateKey, self.privateKeyPassPhrase, True, False)
138            processor.setClaims(
139                self._get_option_from_options(options, 'required_claims'),
140                self._get_option_from_options(options, 'optional_claims'))
141            processor.setOptions(options)
142            options['processorTag'] = processor
143#       self.log.debug("For %s required: %s, optional: %s" %
144#           (tag, self._get_option_from_options(options, 'required_claims'),
145#               self._get_option_from_options(options, 'optional_claims')))
146        return options
147
148    def authenticate(self, req):
149        """called when req.authname is accessed, since this is actually called
150        by many processes in trac, it will often be the time when the security
151        token is read and processed.  """
152        #self.log.debug('web_ui:LoginModule:authenticate' )
153        authnanme = None
154        if req.method == 'POST' and req.path_info.startswith('/login'):
155            #try:
156            #   self.log.debug('web_ui:LoginModule:authenticate:: posted request to login for \"%s\"', req.environ['REMOTE_USER'])
157            #except (NameError, KeyError):
158                req.environ['REMOTE_USER'] = self._remote_user(req)
159                #self.log.debug('web_ui:LoginModule:authenticate:: remote: \"%s\", req.environ[\'REMOTE_USER\']: \"%s\"', req.remote_user, req.environ['REMOTE_USER'])             
160        authname = auth.LoginModule.authenticate(self, req)
161        #self.log.debug('web_ui:LoginModule:authenticate:: authname = \"%s\"', authname)
162        return authname
163
164    authenticate = if_enabled(authenticate)
165
166    def match_request(self, req):
167        """See if we handle the request, we watch for login/logout"""
168        if if_enabled(auth.LoginModule.match_request) \
169            and ( (re.match(r'/login/?$', req.path_info) is not None) \
170                or (re.match(r'/logout/?$', req.path_info) is not None)):
171            return True
172
173        return False
174
175    def process_request(self, req):
176        """ handle display of the login page, display of login results,
177        and logout requests"""
178
179#       self.log.debug('web_ui:LoginModule:process_request : %s' %
180#           (req.path_info))
181        if req.path_info.startswith('/logout'):
182            self._cleanup_session(req)
183        if req.path_info.startswith('/login'):
184            if req.authname == 'anonymous':
185
186                data = {
187                    'title': 'Login',
188                    'login_header': 'Login',
189                    'error_header': 'Error',
190                    'referer': self._referer(req),
191                    'reset_password_enabled': AccountModule(self.env).reset_password_enabled,
192                    'submit_text': 'Login',
193                    'display_infocard': True,
194                    'infocards': self.processors,
195                    'help_text': self.config.get('infocard_acct', 'up_help_text', 'Use existing credentials to authenticate to your account'),
196                    'up_show': self.config.getbool('infocard_acct', 'up_show', True)
197                }
198
199                if not req.args.get('xmlToken') and not req.args.get('cardkeyhash'):
200                    data['display_infocard'] = True
201                    if req.method == 'POST':
202                        data['login_error'] = 'Invalid username or password'
203                else:
204                    secToken = self._get_token(req)
205                    cardkeyhash = None
206                    if req.args.get('cardkeyhash'):
207                        cardkeyhash = req.args.get('cardkeyhash')
208                    elif secToken:
209                        if secToken and secToken.isValid:
210                            cardkeyhash = secToken.getMetaDataValues(infocard.infocardlib.META_CardKeyHash)
211                            self.log.debug('setup session: pre associate')
212                            self._setup_session(req)
213                        elif secToken:
214                            #invalid security token, handle error
215                            data['infocard'] = secToken
216                            data['title'] = 'Invalid Infocard Detail'
217                            if secToken.eventLog:
218                                data['events'] = secToken.eventLog.events
219                            return 'infocard-detail.html', data, None
220
221                    data['cardkeyhash'] = cardkeyhash
222                    data['title'] = 'Associate'
223                    data['login_header'] = 'Login to associate an InfoCard with an Account'
224                    data['error_header'] = 'Warning',
225                    data['login_error'] = 'Credentials have not been associated with your account'
226                    data['submit_text'] = 'Associate'
227                    data['blockID'] = 'infocard_acct'
228                    data['display_infocard'] = False
229                    data['help_text'] = self.config.get('infocard_acct', 'associate_help_text')
230                    data['up_show'] = True
231                return 'authenticate.html', data, None
232            elif req.args.get('xmlToken'):
233                secToken = self._get_token(req)
234                data = {'infocard': secToken}
235
236                if self._get_option(req, 'debug_page'):
237                    self.log.debug('debug_page %s', req.remote_user)
238                    self._do_debug_login(req)
239                    self._setup_session(req)
240                    data['title'] = 'Infocard Debug'
241                if not secToken.isValid:
242                    data['title'] = 'Invalid Infocard Detail'
243                if secToken.eventLog:
244                    data['events'] = secToken.eventLog.events
245                if (not secToken.isValid) or self._get_option(req, 'debug_page'):
246                    return 'infocard-detail.html', data, None
247                if not req.remote_user:
248                    self._remote_user(req)
249                self._setup_session(req)
250            elif not self._remote_user(req):
251                self._redirect_back(req)
252        return auth.LoginModule.process_request(self, req)
253
254    def _cleanup_session(self, req):
255        """We need to delete session attributes here, allow all people who
256        potentially cached information from the login to act on the logout
257        """
258        SecTokenSessionModule(self.env).logout(req)
259
260    def _setup_session(self, req):
261        """setup session with infocard variables so we can get back to them
262        the infocard must be parsed and availible on this request
263        """
264        SecTokenSessionModule(self.env).login(req, self._get_token(req))
265
266    def _do_debug_login(self, req):
267        """this emulates the code in trac.web.auth.py, but doesn't do the redirect
268        It should be only used for the debug login.
269        """
270
271        ignore_case = BoolOption('trac', 'ignore_auth_case', 'false',
272        """Whether case should be ignored for login names (''since 0.9'').""")
273
274        remote_user = req.remote_user
275        if self.ignore_case:
276            remote_user = remote_user.lower()
277        cookie = hex_entropy()
278        db = self.env.get_db_cnx()
279        cursor = db.cursor()
280        cursor.execute("INSERT INTO auth_cookie (cookie,name,ipnr,time) "
281            "VALUES (%s, %s, %s, %s)", (cookie, remote_user,
282            req.remote_addr, int(time.time())))
283        db.commit()
284
285        req.authname = remote_user
286        req.outcookie['trac_auth'] = cookie
287        req.outcookie['trac_auth']['path'] = req.href()
288
289    def _get_option_from_options(self, options, tag):
290        if options and options.has_key(tag):
291            return options[tag]
292        return None
293
294    def _get_option(self, req, tag):
295        """Manage the options lists for all infocard login configs 
296        """
297        block = req.args.get('blockID')
298        if not block:
299            block = 'infocard_acct'
300        try:
301            options = self.processors[block]
302            return self._get_option_from_options(options, tag)
303        except (NameError, KeyError):
304            pass
305
306    def _get_token(self, req):
307        """get the security token, cache it on the request so it isn't reparsed
308        """
309
310        secToken = None
311        try:
312            secToken = req.environ['Security_Token']
313        except (NameError, KeyError):
314            processor = self._get_option(req, 'processorTag')
315            transport = 'http'
316            referer = self._referer(req)
317            if referer and referer.startswith('https'):
318                transport = 'https'
319            secToken = processor.processToken(req.args.get('xmlToken'), transport)
320            req.environ['Security_Token'] = secToken
321        return secToken
322
323    def _remote_user(self, req):
324        """ Called to determine the name of the user, it's poorly named.  It has
325        the side effect of actually verifying username/password or credentials
326        """
327
328#        self.log.debug('web_ui:LoginModule:_remote_user' )
329        user = req.args.get('user')
330        password = req.args.get('password')
331        #if user and password:
332        #   self.log.debug('web_ui:LoginModule:_remote_user :' + user + ' : ' + password)
333        if user or req.args.get('xmlToken'):
334            if user and AccountManager(self.env).check_password(user, password):
335                if req.args.get('cardkeyhash'):
336                    associateduser = AssociationManager(self.env).check_association(req.args.get('cardkeyhash'))
337                    if associateduser:
338                        if associateduser != user:
339                            self.log.debug('web_ui:LoginModule:_remote_user'
340                                '\"%s\" != \"%s\"', user, associateduser)
341                            return None
342                    else :
343                        self.log.debug('web_user:LoginModule:_remote_user:set_association for \"%s\"', user)
344                        AssociationManager(self.env).set_association(user, req.args.get('cardkeyhash'))
345#                self.log.debug('web_ui:LoginModule:_remote_user check_password worked returning '
346#                                '\"%s\"', user)
347                return user
348            elif req.args.get('xmlToken') and req.path_info.startswith('/login'):
349                #self.log.debug('web_user:LoginModule:_remote_user parsing token')
350                secToken = self._get_token(req)
351                if secToken and secToken.isValid:
352                    user = AssociationManager(self.env).check_association(secToken.getMetaDataValues(infocard.infocardlib.META_CardKeyHash))
353                    if user:
354#                        self.log.debug('web_user:LoginModule:_remote_user:check_association returned : \"%s\"', user)
355                        return user
356                    else:
357                        associateduser = self._get_option(req, 'associated_user')
358                        if associateduser:
359                            return associateduser
360                        self.log.debug('web_ui:LoginModule:_remote_user:check_association failed')
361        self.log.debug('web_ui:LoginModule:_remote_user: -> None')
362        return None
363
364    def _redirect_back(self, req):                                                         
365        """Redirect the user back to the URL she came from, only used during
366        debug logins, otherwise we allow trac to handle the request redirect."""
367        referer = self._referer(req)
368        if referer and ((not referer.startswith(req.base_url))
369            or (referer.endswith("login"))):
370            # don't redirect to external sites
371            referer = None
372        req.redirect(referer or self.env.abs_href())
373
374    def _referer(self, req):
375        return req.args.get('referer') or req.get_header('Referer')
376
377    def enabled(self):
378        # Users should disable the built-in authentication to use this one
379        return not (self.env.is_component_enabled(auth.LoginModule) \
380        or self.env.is_component_enabled(web_ui.LoginModule))
381    enabled = property(enabled)
382
383    # ITemplateProvider
384
385    def get_htdocs_dirs(self):
386        """Return the absolute path of a directory containing additional
387        static resources (such as images, style sheets, etc).
388        """
389        from pkg_resources import resource_filename
390        return [('infocard_acct', resource_filename(__name__, 'htdocs')),
391           ('site', self.env.get_htdocs_dir())]
392
393    def get_templates_dirs(self):
394        """Return the absolute path of the directory containing the provided
395        templates.
396        """
397        from pkg_resources import resource_filename
398        return [resource_filename(__name__, 'templates')]
Note: See TracBrowser for help on using the browser.