Closed Bug 663221 Opened 14 years ago Closed 14 years ago

Update account-portal to the new user spec

Categories

(Cloud Services :: Server: Account Portal, defect)

x86
macOS
defect
Not set
normal

Tracking

(Not tracked)

VERIFIED FIXED

People

(Reporter: telliott, Assigned: telliott)

References

Details

Attachments

(2 files)

new spec with the user object needs a-p changes
As a special bonus, abstracting out the reset codes made testing the rest of the forgot password flow easier.
Assignee: nobody → telliott
Attachment #538341 - Flags: review?(jrconlin)
Attachment #538341 - Flags: feedback?(tarek)
No longer blocks: 659132
Depends on: 659132
Comment on attachment 538341 [details] [diff] [review] rewrites the auth bits to use the new auth module * While I appreciate the reason, probably ought to drop old code in account.py:95..104 * probably want to use a generic email in the tests.
Attachment #538341 - Flags: review?(jrconlin) → review+
ironically, 95...104 isn't old code. It's code that gets uncommented when pete finishes the ldap dn change.
Comment on attachment 538341 [details] [diff] [review] rewrites the auth bits to use the new auth module >diff -r 72248e493553 AccountPortal.spec >--- a/AccountPortal.spec Thu Jun 02 16:11:20 2011 -0700 >+++ b/AccountPortal.spec Thu Jun 09 13:47:53 2011 -0700 >@@ -5,7 +5,7 @@ > Group: Mozilla > License: MPL > Packager: Toby Elliott <telliott@mozilla.com> >-Source0: AccountPortal-1.2.tar.gz >+Source0: AccountPortal-%{version}.tar.gz > Requires: python26 python26-setuptools python26-webob python26-paste python26-pastedeploy python26-pastescript python26-services python26-beaker python26-memcached python26-simplejson python26-wsgi_intercept python26-beautifulsoup python26-ldap python26-pycryptopp python26-recaptcha-client python26-cef > BuildRoot: %{_tmppath}/%{pythonname}-%{version}-%{release}-buildroot > >@@ -16,7 +16,7 @@ > rm -rf $RPM_BUILD_ROOT > > %prep looks like something we can commit right away on its own >-%setup -n AccountPortal-1.2 >+%setup -n AccountPortal-%{version} > > %build > python2.6 setup.py build >diff -r 72248e493553 accountportal/controllers/__init__.py >--- a/accountportal/controllers/__init__.py Thu Jun 02 16:11:20 2011 -0700 >+++ b/accountportal/controllers/__init__.py Thu Jun 09 13:47:53 2011 -0700 >@@ -42,7 +42,7 @@ > > from hashlib import sha256 > from time import time >-from services.auth import get_auth >+from services.pluginreg import load_and_configure > from cef import log_cef > from accountportal import logger > from mako.lookup import TemplateLookup >@@ -77,10 +77,15 @@ > def __init__(self, conf): > self.config = conf > try: >- self.auth = get_auth(self.config) >+ self.auth = load_and_configure(self.config, 'auth') > except Exception, e: > logger.error("unable to load auth. Problem? %s" % e) > self.auth = None >+ try: >+ self.reset = load_and_configure(self.config, 'reset_codes') >+ except Exception, e: do str(e) here or you might not get the error. Also if you want to log the traceback, use the traceback module (see how I do in services.util or services.baseapp) >+ logger.error("unable to load reset codes. Problem? %s" % e) >+ self.reset = None > > def render(self, template, **data): > return self._lookup.get_template(template).render(**data) >diff -r 72248e493553 accountportal/controllers/account.py >--- a/accountportal/controllers/account.py Thu Jun 02 16:11:20 2011 -0700 >+++ b/accountportal/controllers/account.py Thu Jun 09 13:47:53 2011 -0700 >@@ -38,6 +38,7 @@ > from gettext import gettext as _ > > from services.util import valid_email, valid_password, extract_username >+from services.user import User > from cef import log_cef > > from accountportal import logger >@@ -82,23 +83,25 @@ > return self.render(template, **data) > > #because changing the email now changes the username, we need to check >- #for collisions >+ #for collisions - NOT YET > new_username = extract_username(new_email) > if self.auth.get_user_id(new_username): > data['error'] = \ > _('The email address you specified is already in use.') > return self.render(template, **data) > >- uid = beaker.get('uid') >- if self.auth.update_email(uid, new_email, beaker.get('password')): >+ if self.auth.update_field(request._user, beaker.get('password'), >+ 'mail', new_email): I agree with JR: commiting commented code smells bad. I'd remove it and introduce it later, since Hg is useful for this >+ #self.auth.update_field(request._user, beaker.get('password'), >+ # 'uid', extract_username(new_email)): > data['success'] = \ > _('Your account name was succesfully changed to %s.' % new_email) > #make sure to change our session to have the new email > beaker['email'] = new_email >- beaker['username'] = new_email >+ #beaker['username'] = extract_username(new_email) > beaker.save() > data['email'] = new_email >- data['username'] = new_email >+ #data['username'] = new_email > else: > data['error'] = \ > _('An unknown problem ocurred. Please try again later.') >@@ -111,7 +114,6 @@ > beaker = request.environ['beaker.session'] > data = self.get_base_data(request) > template = 'change_password.mako' >- uid = beaker.get('uid') > > data['trail'] = [[None, _('Change Password')]] > password = request.params.get('new_password') >@@ -132,7 +134,8 @@ > _('Please make sure your password is at least 8 characters long.') > return self.render(template, **data) > >- if self.auth.update_password(uid, password, beaker.get('password')): >+ if self.auth.update_password(request._user, beaker.get('password'), >+ password): > data['success'] = _('Your password was succesfully changed.') > log_cef('Password Changed', 5, > request.environ, self.config, data['username'], >@@ -165,24 +168,25 @@ > return self.render(template, **data) > > data['crumb'] = self.generate_crumb(request) >- uid = self.auth.authenticate_user(username, pwd) >- if not uid: >+ if not self.auth.authenticate_user(request._user, pwd): > data['error'] = _('We were unable to authenticate your account.') > return self.render(template, **data) > > #if this supports a sync cluster, need to purge from there > if "sync" in self.config.get('console.modules'): >- client = SyncClient(self.config, >- self.auth.get_user_node(uid, False), >- username, >- beaker.get('password')) >- if not client.delete_data(): >- data['alert'] = \ >- _("We were unable to delete your data on the weave node. " >- "Don't worry, it's encrypted on the node and will be " >- "cleaned up shortly.") >+ self.auth.get_user_info(request._user, ['syncNode']) >+ if request._user.get('syncNode', None): >+ client = SyncClient(self.config, >+ request._user['syncNode'], >+ request._user['userName'], >+ pwd) >+ if not client.delete_data(): >+ data['alert'] = \ >+ _("We were unable to delete your data on the weave node. " >+ "Don't worry, it's encrypted on the node and will be " >+ "cleaned up shortly.") > >- if not self.auth.delete_user(uid, pwd): >+ if not self.auth.delete_user(request._user, pwd): > data['error'] = _('Deleting your account failed unexpectedly. ' > 'Please try again later.') > return self.render(template, **data) >diff -r 72248e493553 accountportal/controllers/console.py >--- a/accountportal/controllers/console.py Thu Jun 02 16:11:20 2011 -0700 >+++ b/accountportal/controllers/console.py Thu Jun 09 13:47:53 2011 -0700 >@@ -35,8 +35,10 @@ > # ***** END LICENSE BLOCK ***** > from gettext import gettext as _ > >-from services.auth import NoEmailError, InvalidCodeError >-from services.util import valid_password, extract_username >+from services.auth import NoEmailError >+from services.resetcodes import AlreadySentError, InvalidCodeError >+from services.util import (valid_password, extract_username, >+ send_email, valid_email, filter_params) > from cef import log_cef, AUTH_FAILURE, CAPTCHA_FAILURE > > from accountportal import logger >@@ -67,58 +69,64 @@ > > beaker = request.environ['beaker.session'] > if beaker.get('uid'): >- request.remote_user = beaker.get('username') >+ request._user['userName'] = beaker.get('username') >+ request._user['userId'] = beaker.get('uid') > return True, '' > > data = self.get_base_data(request) > template = 'login.mako' > > if 'username' in request.POST: # trying to log in >- username = extract_username(request.POST.get('username')) >+ request._user['userName'] = \ >+ extract_username(request.POST.get('username')) > pwd = request.POST.get('password') > if not pwd: > data['error'] = _('Password required') > return False, self.render(template, **data) > >- uid = self.auth.authenticate_user(username, pwd) >+ uid = self.auth.authenticate_user(request._user, pwd, ['mail']) > if uid: >- __, email = self.auth.get_user_info(uid) > beaker['uid'] = uid >- beaker['email'] = email >- beaker['username'] = username >+ beaker['email'] = request._user['mail'] >+ beaker['username'] = request._user['userName'] > beaker['password'] = pwd > beaker.save() >- request.remote_user = username >+ request._user['userId'] = uid > return True, '' > else: > log_cef('Authentication Failed', 5, >- request.environ, self.config, username, >+ request.environ, self.config, request._user['userName'], > signature=AUTH_FAILURE) > data['error'] = _('We were unable to log you in') > > return False, self.render(template, **data) > >+ def _render_step_1(self, request, data): >+ # preserve it in the form if there's a problem Here you should do this instead of these 3 lines: data['form_username'] = request.params.get('username', '') >+ data['form_username'] = '' >+ if request.params.get('username', None): >+ data['form_username'] = request.params['username'] >+ data['captcha'] = request._captcha.form() >+ return self.render('password_reset1.mako', **data) >+ >+ > def forgot(self, request, **args): > """Multi-step process to retrieve a forgotten password""" > > data = self.get_base_data(request) >- data['form_username'] = '' > > #only page with no userid is the starting one > if not request.params.get('username'): >- data['captcha'] = request._captcha.form() >- return self.render('password_reset1.mako', **data) >+ return self._render_step_1(request, data) > > username = extract_username(request.params['username']) >- user_id = self.auth.get_user_id(username) >- if username: # preserve it in the form if there's a problem >- data['form_username'] = request.params['username'] >+ request._user['userName'] = username >+ user_id = self.auth.get_user_id(request._user) > > if not user_id: > data['error'] = \ > _('Unable to locate your account. Please check your username.') >- data['captcha'] = request._captcha.form() >- return self.render('password_reset1.mako', **data) >+ return self._render_step_1(request, data) > > #if no reset code, time to ship them one > if not request.params.get('key'): >@@ -128,19 +136,39 @@ > signature=CAPTCHA_FAILURE) > data['error'] = _('The captcha did not match. ' > 'Please try again') >- data['captcha'] = request._captcha.form() >- return self.render('password_reset1.mako', **data) >+ return self._render_step_1(request, data) > > try: >- if not self.auth.generate_reset_code(user_id): >+ reset_code = self.reset.generate_reset_code(request._user, True) >+ if not reset_code: > data['error'] = _('Getting a reset code failed ' > 'unexpectedly. Please try again later.') > logger.error("Could not generate a reset code") >- return self.render('password_reset1.mako', **data) >+ return self._render_step_1(request, data) >+ self.auth.get_user_info(request._user, ['mail']) >+ if not valid_email(request._user['mail']): >+ raise NoEmailError() >+ >+ maildata = {'forgot_url': '%s/forgot' % request.host_url, >+ 'username': username, >+ 'code': reset_code >+ } >+ body = self.render('password_reset_mail.mako', **maildata) >+ subject = _('Resetting your Mozilla Services password') >+ smtp = filter_params('smtp', self.config) >+ #sender has a required position, so we can't pass it in in the >+ #dict >+ del smtp['sender'] >+ send_email(self.config['smtp.sender'], request._user['mail'], >+ subject, body, **smtp) no need for ",e" >+ except AlreadySentError, e: >+ #backend handled the reset code email. Keep going >+ pass no need for ",e" > except NoEmailError, e: > data['error'] = _('We do not have an email on file for this ' > 'account and cannot send you a reset code.') >- return self.render('password_reset1.mako', **data) >+ return self._render_step_1(request, data) > > return self.render('password_reset2.mako', **data) > >@@ -149,38 +177,47 @@ > data['key'] = code > data['usernm'] = username > >+ #validate the access code >+ #NOTE: sreg automatically returns true here, and defers code validation >+ #until later >+ if not self.reset.verify_reset_code(request._user, code): >+ data['error'] = _('The reset code you submitted was ' >+ 'invalid. Please request a new one.') >+ return self._render_step_1(request, data) >+ > password = request.params.get('new_password') >- if password: >- #verify that password and confirm match >- confirm = request.params.get('confirm_password') >- if password != confirm: >- data['error'] = _('The new password and confirmation do ' >- 'not match. Please try again.') >+ if not password: >+ #no new password, so give them the password form >+ return self.render('password_reset3.mako', **data) >+ >+ #verify that password and confirm match >+ confirm = request.params.get('confirm_password') >+ if password != confirm: >+ data['error'] = _('The new password and confirmation do ' >+ 'not match. Please try again.') >+ return self.render('password_reset3.mako', **data) >+ >+ if not valid_password(username, password): >+ data['error'] = _('The new password is not valid. ' >+ 'Please try again.') >+ return self.render('password_reset3.mako', **data) >+ >+ try: >+ if not self.auth.admin_update_password(request._user, password, code): >+ data['error'] = _('Changing the password failed. ' >+ 'Please ask for a new key and try again later') > return self.render('password_reset3.mako', **data) No need for ",e" >+ except InvalidCodeError, e: >+ data['error'] = _('The reset code you submitted was ' >+ 'invalid. Please request a new one.') >+ return self._render_step_1(request, data) > >- if not valid_password(username, password): >- data['error'] = _('The new password is not valid. ' >- 'Please try again.') >- return self.render('password_reset3.mako', **data) >+ log_cef('Password Changed', 5, >+ request.environ, self.config, username, >+ signature="PasswordReset") >+ self.reset.clear_reset_code(request._user) >+ return self.render('password_reset4.mako', **data) > >- try: >- if not self.auth.admin_update_password(user_id, password, key=code): >- data['error'] = _('Changing the password failed. ' >- 'Please ask for a new key and try again later') >- return self.render('password_reset3.mako', **data) >- except InvalidCodeError, e: >- data['error'] = _('The reset code you submitted was ' >- 'invalid. Please request a new one.') >- return self.render('password_reset1.mako', **data) >- >- log_cef('Password Changed', 5, >- request.environ, self.config, username, >- signature="PasswordReset") >- >- return self.render('password_reset4.mako', **data) >- >- #no new password, so give them the password form >- return self.render('password_reset3.mako', **data) > > def console(self, request, **args): > """Shows the home page. Not much to do there right now""" >diff -r 72248e493553 accountportal/controllers/sync.py >--- a/accountportal/controllers/sync.py Thu Jun 02 16:11:20 2011 -0700 >+++ b/accountportal/controllers/sync.py Thu Jun 09 13:47:53 2011 -0700 >@@ -38,8 +38,6 @@ > from gettext import gettext as _ > import re > >-from services.auth import get_auth >- > from accountportal import logger > from accountportal.controllers import BaseController, get_template_lookup > from accountportal.clients.sync import SyncClient >@@ -91,20 +89,20 @@ > data['success'] = 1 > > else: >- uid = beaker.get('uid') >- client = SyncClient(self.config, >- self.auth.get_user_node(uid, False), >- beaker.get('username'), >- beaker.get('password')) >- results = client.get_quota_used() >+ data['quota'] = '' >+ self.auth.get_user_info(request._user, ['syncNode']) >+ if request._user.get('syncNode', None): >+ client = SyncClient(self.config, >+ request._user['syncNode'], >+ request._user['userName'], >+ beaker.get('password')) >+ results = client.get_quota_used() > >- if results[0] is not None: >- data['quota'] = "%s %sK" % (_('You are currently using'), >- results[0]) >- if results[1] is not None: >- data['quota'] += " %s %sK" % (_('out of'), results[1]) >- else: >- data['quota'] = '' >+ if results[0] is not None: >+ data['quota'] = "%s %sK" % (_('You are currently using'), >+ results[0]) >+ if results[1] is not None: >+ data['quota'] += " %s %sK" % (_('out of'), results[1]) > > data['crumb'] = self.generate_crumb(request) > return self.render(template, **data) >@@ -119,19 +117,18 @@ > data['trail'] = [[None, _('Purge Sync')]] > purge = request.params.get('purge') > if purge and self.check_crumb(request): >- if self.auth is None: >- self.auth = get_auth(self.config) >- >- uid = beaker.get('uid') >- client = SyncClient(self.config, >- self.auth.get_user_node(uid, False), >- beaker.get('username'), >- beaker.get('password')) >- if not client.delete_data(): >- data['alert'] = _('We had a problem purging the data from your' >- + ' account. Please try again later.') >- else: >- data['success'] = 1 >+ self.auth.get_user_info(request._user, ['syncNode']) >+ if request._user.get('syncNode', None): >+ client = SyncClient(self.config, >+ request._user['syncNode'], >+ request._user['userName'], >+ beaker.get('password')) >+ if not client.delete_data(): >+ data['alert'] = _('We had a problem purging the data ' >+ 'from your account. Please try again ' >+ 'later.') >+ else: >+ data['success'] = 1 > > data['crumb'] = self.generate_crumb(request) > return self.render(template, **data) >diff -r 72248e493553 accountportal/templates/console/password_reset_mail.mako >--- a/accountportal/templates/console/password_reset_mail.mako Thu Jun 02 16:11:20 2011 -0700 >+++ b/accountportal/templates/console/password_reset_mail.mako Thu Jun 09 13:47:53 2011 -0700 >@@ -1,6 +1,6 @@ > ${_('You asked to reset your Mozilla Services password. To do so, please click this link')}: > >- ${host}/console/forgot?username=${user_name | u}&key=${code |u} >+ ${forgot_url}?username=${username | u}&key=${code |u} > > ${_("This will let you change your password to something new. If you didn't ask for this, don't worry, we'll keep your password safe.")} > >diff -r 72248e493553 accountportal/tests/base.py >--- a/accountportal/tests/base.py Thu Jun 02 16:11:20 2011 -0700 >+++ b/accountportal/tests/base.py Thu Jun 09 13:47:53 2011 -0700 >@@ -42,7 +42,7 @@ > > from webtest import TestApp > >-from services.auth import get_auth >+from services.pluginreg import load_and_configure > from services.tests.support import TestEnv, patch_captcha > > from accountportal.wsgiapp import make_app >@@ -73,31 +73,28 @@ > return > > self.appdir = env.topdir >- self.auth = get_auth(self.config) >- self.app = TestApp(make_app(self.config)) >+ self.auth = load_and_configure(env.config, 'auth') >+ self.reset = load_and_configure(env.config, 'reset_codes') >+ self.app = TestApp(make_app(env.config)) > self.session = '' >+ > # adding a user if needed > userint = random.randint(1, 1000) > self.username = 'test_user%d' % userint >- self.user_id = self.auth.get_user_id(self.username) > self.email = self.config.get('test_email', > 'telliott+%d@mozilla.com' % userint) > self.password = 'x' * 9 > >- if self.user_id is None: >- self.create_user() >+ self.user = self.auth.create_user(self.username, >+ self.password, self.email) > > def tearDown(self): >- if self.user_id: >- self.auth.delete_user(self.user_id, self.password) >- self.user_id is None >+ if self.user: >+ self.auth.delete_user(self.user, self.password) > cef_logs = os.path.join(self.appdir, 'test_cef.log') > if os.path.exists(cef_logs): > os.remove(cef_logs) > >- def create_user(self): >- self.auth.create_user(self.username, self.password, self.email) >- self.user_id = self.auth.get_user_id(self.username) > > def log_in(self): > if not self.session: >@@ -165,18 +162,47 @@ > good_req = self.app.post(test_url, > {'username': self.username, > 'recaptcha_challenge_field': 'foo', >- 'recaptcha_response_field': 'bar'}) >+ 'recaptcha_response_field': 'bar' >+ }) > assert "We have emailed a reset code" in good_req > >- #can't really test whether we get the email >+ #can't really test whether we get the email, but we can get the reset >+ #code from the db > >- #this test is no longer valid, since we don't verify the key at this stage >- #bad_key_req = self.app.get(test_url, {'username': self.username, >- # 'key': 'bad'}) >- #assert "The reset code you submitted was invalid" in bad_key_req >+ code = self.reset.generate_reset_code(self.user, overwrite=False) >+ good_req = self.app.post(test_url, >+ {'username': self.username, 'key': code}) > >- #we have no method to get the key to submit it, which makes unit >- #testing the rest of this mostly impossible >+ >+ old_password = self.password >+ new_password = 'z' * 9 >+ >+ # clear the session and try to log in again >+ self.log_in() >+ res_in = self.app.get("/") >+ >+ #now change the password >+ good_req = self.app.post(test_url, >+ {'username': self.username, 'key': code, >+ 'new_password': new_password, >+ 'confirm_password': new_password >+ }) >+ assert "Your password has been successfully changed" in good_req >+ >+ self.password = new_password >+ >+ self.log_out() >+ assert self.logged_out >+ >+ res_bad = self.app.post("/", {'username': self.username, >+ 'password': old_password}) >+ assert "We were unable to log you in" in res_bad >+ >+ #log in with the new password >+ self.log_in() >+ res_in2 = self.app.get("/") >+ assert res_in.html == res_in2.html >+ > > def _get_app(self): > return self.app >diff -r 72248e493553 accountportal/wsgiapp.py >--- a/accountportal/wsgiapp.py Thu Jun 02 16:11:20 2011 -0700 >+++ b/accountportal/wsgiapp.py Thu Jun 09 13:47:53 2011 -0700 >@@ -46,6 +46,7 @@ > from accountportal import logger > import accountportal.beakerpatch # NOQA > >+from services.user import User > from services.util import convert_config, CatchErrorMiddleware, filter_params > from services.captcha import ServicesCaptcha > >@@ -137,6 +138,9 @@ > #adding our captcha to the environment > request._captcha = self.captcha > >+ #adding a user object to the environment >+ request._user = User() >+ > match = self.mapper.routematch(environ=request.environ) > if match is None: > return HTTPNotFound() >@@ -176,8 +180,9 @@ > # result is already a Response > response = result > >- if request.remote_user: >- response.headers.add('X-Services-Username', request.remote_user) >+ if request._user.get('userName', None): >+ response.headers.add('X-Services-Username', >+ request._user['userName']) > > response.headers.add('X-Frame-Options', 'DENY') > return response >diff -r 72248e493553 etc/accountportal_test.cfg >--- a/etc/accountportal_test.cfg Thu Jun 02 16:11:20 2011 -0700 >+++ b/etc/accountportal_test.cfg Thu Jun 09 13:47:53 2011 -0700 >@@ -30,8 +30,18 @@ > timeout = 20 > > [auth] >-backend = sql >+backend = services.user.sql.SQLUser > sqluri = sqlite:////tmp/account.db >+create_tables = True >+ >+[nodes] >+backend = syncnodes.admincontroller.AdminController >+sqluri = sqlite:////tmp/account.db >+ >+[reset_codes] >+backend = services.resetcodes.rc_sql.ResetCodeSQL >+sqluri = sqlite:////tmp/reset.db >+create_tables = True > > [cef] > use = true >@@ -39,8 +49,6 @@ > vendor = mozilla > version = 0 > device_version = 1.3 >- >-[cef] > product = portal > > [captcha] >diff -r 72248e493553 tests.ini >--- a/tests.ini Thu Jun 02 16:11:20 2011 -0700 >+++ b/tests.ini Thu Jun 09 13:47:53 2011 -0700 >@@ -1,6 +1,7 @@ > [DEFAULT] > debug = True > translogger = False >+extends = etc/accountportal_test.cfg > > [server:main] > use = egg:Paste#http >@@ -9,7 +10,6 @@ > > [app:main] > use = egg:AccountPortal >-configuration = file:%(here)s/etc/accountportal_test.cfg > > # Logging configuration > [loggers]
Attachment #538341 - Flags: feedback?(tarek) → feedback+
Comment on attachment 541313 [details] [diff] [review] The rewritten account portal that takes advantage of our new core changes Minor nit: Having seen this happen, I'll agree with Tarek that you want to str() any generic Exception you catch when you log it. Bad exceptions don't always render correctly.
Attachment #541313 - Flags: review?(jrconlin) → review+
Pushed in http://hg.mozilla.org/services/account-portal/rev/9cf80e83f1a6 Note that this requires server-core v2.0+ and I've put that in setup.py to ensure it
Status: NEW → RESOLVED
Closed: 14 years ago
Resolution: --- → FIXED
Status: RESOLVED → VERIFIED
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: