Ignore:
Timestamp:
14 Sep 2011, 01:26:25 (13 years ago)
Author:
uli
Message:

Merge changes from uli-studentpw back into trunk.

Location:
main/waeup.sirp/trunk
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • main/waeup.sirp/trunk

  • main/waeup.sirp/trunk/src/waeup/sirp/students/authentication.py

    r6680 r6756  
    2626from zope.component import getUtility
    2727from zope.password.interfaces import IPasswordManager
    28 from zope.pluggableauth.interfaces import IAuthenticatorPlugin
     28from zope.pluggableauth.interfaces import (
     29    IAuthenticatorPlugin, ICredentialsPlugin)
     30from zope.pluggableauth.plugins.session import (
     31    SessionCredentialsPlugin, SessionCredentials)
     32from zope.publisher.interfaces.http import IHTTPRequest
     33from zope.session.interfaces import ISession
    2934from waeup.sirp.authentication import PrincipalInfo, get_principal_role_manager
    3035from waeup.sirp.interfaces import IAuthPluginUtility, IUserAccount
     
    157162        return IUserAccount(student)
    158163
     164class PasswordChangeCredentialsPlugin(grok.GlobalUtility,
     165                                      SessionCredentialsPlugin):
     166    """A session credentials plugin that handles the case of a user
     167       changing his/her own password.
     168
     169    When users change their own password they might find themselves
     170    logged out on next request.
     171
     172    To avoid this, we support to use a 'change password' page a bit
     173    like a regular login page. That means, on each request we lookup
     174    the sent data for a login field called 'student_id' and a
     175    password.
     176
     177    If both exist, this means someone sent new credentials.
     178
     179    We then look for the old credentials stored in the user session.
     180    If the new credentials' login (the student_id) matches the old
     181    one's, we set the new credentials in session _but_ we return the
     182    old credentials for the authentication plugins to check as for the
     183    current request (and for the last time) the old credentials apply.
     184
     185    No valid credentials are returned by this plugin if one of the
     186    follwing circumstances is true
     187
     188    - the sent request is not a regular IHTTPRequest
     189
     190    - the credentials to set do not match the old ones
     191
     192    - no credentials are sent with the request
     193
     194    - no credentials were set before (i.e. the user has no session
     195      with credentials set before)
     196
     197    Therefore it is mandatory to put this plugin in the line of all
     198    credentials plugins _before_ other plugins, so that the regular
     199    credentials plugins can drop in as a 'fallback'.
     200
     201    This plugin was designed for students to change their passwords,
     202    but might be used to allow password resets for other types of
     203    accounts as well.
     204    """
     205    grok.provides(ICredentialsPlugin)
     206    grok.name('student_pw_change')
     207
     208    loginpagename = 'login'
     209    loginfield = 'student_id'
     210    passwordfield = 'form.password'
     211
     212    def extractCredentials(self, request):
     213        if not IHTTPRequest.providedBy(request):
     214            return None
     215        login = request.get(self.loginfield, None)
     216        password = request.get(self.passwordfield, None)
     217        if not login or not password:
     218            return None
     219        session = ISession(request)
     220        sessionData = session.get(
     221            'zope.pluggableauth.browserplugins')
     222        old_credentials = sessionData.get('credentials', None)
     223        if old_credentials is None:
     224            # Password changes for already authenticated users only!
     225            return None
     226        if old_credentials.getLogin() != login:
     227            # Special treatment only for users that change their own pw.
     228            return None
     229        old_credentials = {
     230            'login': old_credentials.getLogin(),
     231            'password': old_credentials.getPassword()}
     232
     233        # Set new credentials in session. These will be active on next request
     234        new_credentials = SessionCredentials(login, password)
     235        sessionData['credentials'] = new_credentials
     236
     237        # Return old credentials for this one request only
     238        return old_credentials
     239
    159240class StudentsAuthenticatorSetup(grok.GlobalUtility):
    160241    """Register or unregister student authentication for a PAU.
     
    165246
    166247    def register(self, pau):
     248        plugins = list(pau.credentialsPlugins)
     249        # this plugin must come before the regular credentials plugins
     250        plugins.insert(0, 'student_pw_change')
     251        pau.credentialsPlugins = tuple(plugins)
    167252        plugins = list(pau.authenticatorPlugins)
    168253        plugins.append('students')
  • main/waeup.sirp/trunk/src/waeup/sirp/students/browser.py

    r6750 r6756  
    6363    WAeUPObjectWidget, WAeUPObjectDisplayWidget)
    6464from waeup.sirp.students.interfaces import (
    65     IStudentsContainer, IStudent, IStudentClearance,
     65    IStudentsContainer, IStudent, IStudentClearance, IStudentPasswordSetting,
    6666    IStudentPersonal, IStudentBase, IStudentStudyCourse,
    6767    IStudentPayments, IStudentAccommodation, IStudentNavigation,
     
    160160        self.acseries = self.request.form.get('form.acseries', None)
    161161        self.acnumber = self.request.form.get('form.acnumber', None)
    162        
     162
    163163        if SUBMIT is None:
    164164            return
     
    294294    grok.require('waeup.viewStudent')
    295295    grok.template('basepage')
    296     form_fields = grok.AutoFields(IStudentBase)  #.omit('password')
     296    form_fields = grok.AutoFields(IStudentBase).omit('password')
    297297    pnav = 4
    298298    title = 'Base Data'
     
    541541    target = 'bedit'
    542542
     543class StudentPasswordSetting(grok.Adapter):
     544    """Adapt IStudent to data needed for password settings.
     545
     546    We provide password getters/setters for the attached context (an
     547    IStudent object) that cooperate seamless with the usual
     548    formlib/form techniques.
     549    """
     550    grok.context(IStudent)
     551    grok.provides(IStudentPasswordSetting)
     552
     553    def __init__(self, context):
     554        self.name = context.name
     555        self.password_repeat = context.password
     556        self.context = context
     557        return
     558
     559    def getPassword(self):
     560        return self.context.password
     561
     562    def setPassword(self, password):
     563        IUserAccount(self.context).setPassword(password)
     564        return
     565
     566    password = property(getPassword, setPassword)
     567
    543568class StudentBaseEditFormPage(WAeUPEditFormPage):
    544569    """ View to edit student base data by student
     
    547572    grok.name('bedit')
    548573    grok.require('waeup.handleStudent')
    549     form_fields = grok.AutoFields(IStudentBaseEdit).omit(
    550         'student_id', 'reg_number', 'matric_number')
     574    #form_fields = grok.AutoFields(IStudentBaseEdit).omit(
     575    #    'student_id', 'reg_number', 'matric_number')
     576    form_fields = grok.AutoFields(IStudentPasswordSetting)
    551577    grok.template('baseeditpage')
    552578    label = 'Change password'
     
    555581
    556582    def update(self):
    557         datepicker.need() # Enable jQuery datepicker in date fields.
    558583        super(StudentBaseEditFormPage, self).update()
    559584        self.wf_info = IWorkflowInfo(self.context)
    560585        return
    561586
    562     @grok.action('Save')
     587    def onFailure(self, action, data, errors):
     588        new_status = []
     589        other_errors = False
     590        for error in errors:
     591            msg = getattr(error, 'message', '')
     592            if isinstance(msg, basestring) and msg != '':
     593                new_status.append(msg)
     594            else:
     595                other_errors = True
     596        if other_errors:
     597            if new_status:
     598                new_status.append('see below for further errors')
     599            else:
     600                new_status.append('See below for details.')
     601        if new_status:
     602            self.status = u'There were errors: %s' % ', '.join(new_status)
     603        return
     604
     605    @grok.action('Save', failure=onFailure)
    563606    def save(self, **data):
    564607        form = self.request.form
    565608        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
    566         if form.has_key('password') and form['password']:
    567             if form['password'] != form['control_password']:
    568                 self.flash('Passwords do not match.')
    569                 return
    570             IUserAccount(self.context).setPassword(form['password'])
     609        changed_fields = self.applyData(self.context, **data)
     610        # Turn list of lists into single list
     611        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
     612        changed_fields = [x for x in changed_fields
     613                          if not x.startswith('password')]
     614        if form.get('form.password', u'') != u'':
    571615            self.context.loggerInfo(ob_class, 'password changed')
    572         changed_fields = self.applyData(self.context, **data)
    573         changed_fields = changed_fields.values()
    574         fields_string = '+'.join(' + '.join(str(i) for i in b) for b in changed_fields)
    575         self.context._p_changed = True
    576         self.flash('Form has been saved.')
     616            self.flash('Form has been saved.')
     617        fields_string = ' + '.join(changed_fields)
    577618        if fields_string:
    578619            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
     
    614655    grok.name('cedit')
    615656    grok.require('waeup.handleStudent')
    616     form_fields = grok.AutoFields(IStudentClearanceEdit).omit('clearance_locked')
     657    form_fields = grok.AutoFields(
     658        IStudentClearanceEdit).omit('clearance_locked')
    617659    #grok.template('clearanceeditpage')
    618660    label = 'Edit clearance data'
     
    667709    pnav = 4
    668710    buttonname = 'Start'
    669    
    670711
    671712    def update(self, SUBMIT=None):
    672         # We must not use form.ac_series and form.ac_number in forms since these
    673         # are interpreted as applicant credentials in the applicants package
     713        # We must not use form.ac_series and form.ac_number in forms
     714        # since these are interpreted as applicant credentials in the
     715        # applicants package
    674716        self.acseries = self.request.form.get('form.acseries', None)
    675717        self.acnumber = self.request.form.get('form.acnumber', None)
  • main/waeup.sirp/trunk/src/waeup/sirp/students/browser_templates/baseeditpage.pt

    r6701 r6756  
    99    tal:define="status view/status"
    1010    tal:condition="status">
    11     Form Status:
    1211    <span i18n:translate="" tal:content="view/status">
    1312      Form status summary
    1413    </span>
    1514  </div>
     15
     16  <input type="hidden" name="student_id" value=""
     17         tal:attributes="value context/student_id" />
    1618
    1719  <table class="form-fields zebra">
     
    3840        </tr>
    3941      </tal:block>
    40       <tr>
    41         <td class="label"><label>Password:</label></td>
    42         <td>
    43           <input name="password" type="password"/>
    44         </td>
    45       </tr>
    46       <tr>
    47         <td class="label"><label>Retype password:</label></td>
    48         <td>
    49           <input name="control_password" type="password" />
    50         </td>
    51       </tr>
    5242    </tbody>
    5343  </table>
  • main/waeup.sirp/trunk/src/waeup/sirp/students/interfaces.py

    r6750 r6756  
    11##
    22## interfaces.py
    3 from zope.interface import Interface, Attribute
     3from zope.interface import Interface, Attribute, invariant
     4from zope.interface.exceptions import Invalid
    45from zope import schema
    56from waeup.sirp.interfaces import IWAeUPObject
     
    5859        """
    5960
    60 class IStudentBase(IWAeUPObject):
    61     """Representation of student base data.
    62     """
    63     history = Attribute('Object history, a list of messages.')
    64     state = Attribute('Returns the registration state of a student')
    65     #student_id = Attribute('Randomly generated id')
    66     password = Attribute('Encrypted password of a student')
    67     adm_code = Attribute('Admission checking access code')
    68 
    69     def loggerInfo(ob_class, comment):
    70         """Adds an INFO message to the log file
    71         """
    72 
    73     student_id = schema.TextLine(
    74         title = u'Student ID',
    75         required = True,
    76         )
    77 
     61class IStudentPasswordSetting(IWAeUPObject):
     62    """Data needed for password setting.
     63    """
    7864    name = schema.TextLine(
    7965        title = u'Full Name',
     
    8268        )
    8369
     70    password = schema.Password(
     71        title = u'New password',
     72        required = False,
     73        )
     74
     75    password_repeat = schema.Password(
     76        title = u'Retype new password',
     77        required = False,
     78        )
     79
     80    @invariant
     81    def passwords_match(obj):
     82        if obj.password == obj.password_repeat:
     83            return
     84        raise Invalid('passwords do not match')
     85
     86class IStudentBase(IWAeUPObject):
     87    """Representation of student base data.
     88    """
     89    history = Attribute('Object history, a list of messages.')
     90    state = Attribute('Returns the registration state of a student')
     91    password = Attribute('Encrypted password of a student')
     92    adm_code = Attribute('Admission checking access code')
     93
     94    password = schema.Password(
     95        title = u'Password',
     96        required = False,
     97        )
     98
     99    def loggerInfo(ob_class, comment):
     100        """Adds an INFO message to the log file
     101        """
     102
     103    student_id = schema.TextLine(
     104        title = u'Student ID',
     105        required = True,
     106        )
     107
     108    name = schema.TextLine(
     109        title = u'Full Name',
     110        default = u'Nobody',
     111        required = True,
     112        )
     113
    84114    reg_number = schema.TextLine(
    85115        title = u'Registration Number',
  • main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_authentication.py

    r6680 r6756  
    2727from zope.password.interfaces import IPasswordManager
    2828from zope.pluggableauth import PluggableAuthentication
     29from zope.security.interfaces import Unauthorized
    2930from zope.securitypolicy.role import Role
    3031from zope.securitypolicy.interfaces import IRole, Allow
     
    3334from waeup.sirp.students.authentication import (
    3435    StudentsAuthenticatorSetup, StudentAccount)
    35 
     36from waeup.sirp.students.tests.test_browser import StudentsFullSetup
     37from waeup.sirp.testing import FunctionalLayer
    3638
    3739class StudentsAuthenticatorSetupTests(unittest.TestCase):
     
    152154        self.assertEqual(self.account.roles, ['waeup.test.Role'])
    153155        return
     156
     157
     158
     159class FunctionalStudentAuthTests(StudentsFullSetup):
     160
     161    layer = FunctionalLayer
     162
     163    def setUp(self):
     164        super(FunctionalStudentAuthTests, self).setUp()
     165        return
     166
     167    def tearDown(self):
     168        super(FunctionalStudentAuthTests, self).tearDown()
     169        return
     170
     171    def test_reset_protected_anonymous(self):
     172        # anonymous users cannot reset others passwords
     173        self.assertRaises(
     174            Unauthorized,
     175            self.browser.open, self.student_path + '/bedit')
     176        return
  • main/waeup.sirp/trunk/src/waeup/sirp/students/tests/test_browser.py

    r6753 r6756  
    279279        # Change password
    280280        self.browser.getLink("Change password").click()
    281         self.browser.getControl(name="password").value = 'new_password'
    282         self.browser.getControl(name="control_password").value = 'new_password'
     281        self.browser.getControl(name="form.password").value = 'new_password'
     282        self.browser.getControl(
     283            name="form.password_repeat").value = 'new_password'
    283284        self.browser.getControl("Save").click()
    284285        self.assertTrue('Form has been saved' in self.browser.contents)
     
    294295        self.browser.open(self.login_path)
    295296        self.browser.getControl(name="form.login").value = self.test_student_id
    296         self.browser.getControl(name="password").value = 'new_password'
     297        self.browser.getControl(name="form.password").value = 'new_password'
    297298        self.browser.getControl("Login").click()
    298299        self.assertEqual(self.browser.url, self.student_path)
Note: See TracChangeset for help on using the changeset viewer.