diff --git a/fas/TODO b/fas/TODO index 90e889d..d4d26b1 100644 --- a/fas/TODO +++ b/fas/TODO @@ -1,14 +1,6 @@ -Things to Fix in FAS2 before declaring it done +Things to Fix in FAS2 before declaring it done: + * fasClient.py: Proper logging -safasprovider.py ----------------- -validate_password(): - We'll want to change this to something that allows for longer passwords - (like md5). The one thing about that is we have to figure out how system - passwords use salt, etc. That way we'll be able to use this with - make_shell_accounts. - -fasClient.py ---------------- -Proper logging +Nice-to-have things: + * fas/group.py: Easy searching within groups (and sponsor/admin interface) diff --git a/fas/dev.cfg b/fas/dev.cfg index 417e454..1347860 100644 --- a/fas/dev.cfg +++ b/fas/dev.cfg @@ -54,7 +54,7 @@ tg.strict_parameters = True server.webpath='/accounts' base_url_filter.on=True -base_url_filter.base_url = "https://publictest10.fedoraproject.org/accounts" +base_url_filter.base_url = "https://publictest10.fedoraproject.org" # Make the session cookie only return to the host over an SSL link # Disabled for testing. diff --git a/fas/fas/model.py b/fas/fas/model.py index c108d1b..1f6107a 100644 --- a/fas/fas/model.py +++ b/fas/fas/model.py @@ -314,7 +314,7 @@ class Groups(SABase): A class method that can be used to search groups based on their email addresses since it is unique. ''' - return cls.query.join(['group_email_purposes', 'group_email']).filter_by(email=email).one() + return cls.query.join(['email_purposes', 'group_email']).filter_by(email=email).one() by_email_address = classmethod(by_email_address) @@ -423,15 +423,15 @@ mapper(People, PeopleTable, properties = { 'email_purposes': relation(EmailPurposes, backref = 'person', collection_class = column_mapped_collection( EmailPurposesTable.c.purpose)), + 'person_emails': relation(PersonEmails, backref = 'person', + collection_class = column_mapped_collection( + PersonEmailsTable.c.email)), 'approved_roles': relation(ApprovedRoles, backref='member', primaryjoin = PeopleTable.c.id==ApprovedRoles.c.person_id), 'unapproved_roles': relation(UnApprovedRoles, backref='member', primaryjoin = PeopleTable.c.id==UnApprovedRoles.c.person_id) }) -mapper(PersonEmails, PersonEmailsTable, properties = { - 'person': relation(People, backref = 'person_emails', uselist = False, - primaryjoin = PeopleTable.c.id==PersonEmailsTable.c.person_id) - }) +mapper(PersonEmails, PersonEmailsTable) mapper(EmailPurposes, EmailPurposesTable, properties = { 'person_email': relation(PersonEmails, uselist = False, primaryjoin = PersonEmailsTable.c.id==EmailPurposesTable.c.email_id) @@ -452,13 +452,13 @@ mapper(Groups, GroupsTable, properties = { 'email_purposes': relation(GroupEmailPurposes, backref = 'group', collection_class = column_mapped_collection( GroupEmailPurposesTable.c.purpose)), + 'group_emails': relation(GroupEmails, backref = 'group', + collection_class = column_mapped_collection( + GroupEmailsTable.c.email)), 'prerequisite': relation(Groups, uselist=False, primaryjoin = GroupsTable.c.prerequisite_id==GroupsTable.c.id) }) -mapper(GroupEmails, GroupEmailsTable, properties = { - 'group': relation(Groups, backref = 'group_emails', uselist = False, - primaryjoin = GroupsTable.c.id==GroupEmailsTable.c.group_id) - }) +mapper(GroupEmails, GroupEmailsTable) mapper(GroupEmailPurposes, GroupEmailPurposesTable, properties = { 'group_email': relation(GroupEmails, uselist = False, primaryjoin = GroupEmailsTable.c.id==GroupEmailPurposesTable.c.email_id) diff --git a/fas/fas/templates/cla/cla.html b/fas/fas/templates/cla/cla.html index 6133ce7..5a5b0e8 100644 --- a/fas/fas/templates/cla/cla.html +++ b/fas/fas/templates/cla/cla.html @@ -2,7 +2,7 @@
Thank you for your interest in The Fedora Project (the "Project"). In order to clarify the intellectual property license granted with Contributions from any person or entity, Red hat, Inc. ("Red Hat"), as maintainer of the Project, must have a Contributor License Agreement (CLA) on file that has been signed by each Contributor, indicating agreement to the license terms below. This license is for Your protection as a Contributor as well as the protection of the Project and its users; it does not change your rights to use your own Contributions for any other purpose.
diff --git a/fas/fas/templates/cla/cla.txt b/fas/fas/templates/cla/cla.txt index bb76078..68e0a18 100644 --- a/fas/fas/templates/cla/cla.txt +++ b/fas/fas/templates/cla/cla.txt @@ -1,6 +1,6 @@ The Fedora Project Individual Contributor License Agreement (CLA) - http://www.fedora.redhat.com/licenses/ + http://fedoraproject.org/wiki/Legal/Licenses/CLA Thank you for your interest in The Fedora Project (the "Project"). In order to clarify the intellectual property license diff --git a/fas/fas/templates/user/email/add.html b/fas/fas/templates/user/email/add.html new file mode 100644 index 0000000..9f063a3 --- /dev/null +++ b/fas/fas/templates/user/email/add.html @@ -0,0 +1,30 @@ + + +${email.email} | ${email.description} | ${_('Verified')} | @@ -36,7 +36,7 @@
${purpose.email} | ${purpose.person_email.description} | ${purpose.purpose} | diff --git a/fas/fas/user.py b/fas/fas/user.py index 8df5ae8..5ca4b01 100644 --- a/fas/fas/user.py +++ b/fas/fas/user.py @@ -3,6 +3,8 @@ from turbogears import controllers, expose, paginate, identity, redirect, widget from turbogears.database import session import cherrypy +import turbomail + import os import re import gpgme @@ -221,7 +223,8 @@ class User(controllers.Controller): target = person if not canEditUser(person, target): turbogears.flash(_('You cannot edit %s') % target.username ) - username = turbogears.identity.current.username + turbogears.redirect('/user/view/%s', target.username) + return dict() return dict(target=target) @identity.require(turbogears.identity.not_anonymous()) @@ -236,7 +239,7 @@ class User(controllers.Controller): if not canEditUser(person, target): turbogears.flash(_("You do not have permission to edit '%s'") % target.username) - turbogears.redirect('/user/edit/%s', target.username) + turbogears.redirect('/user/view/%s', target.username) return dict() try: target.human_name = human_name @@ -286,7 +289,6 @@ class User(controllers.Controller): # Also, perhaps implement a timeout- delete account # if the e-mail is not verified (i.e. the person changes # their password) withing X days. - import turbomail try: person = People() person.username = username @@ -296,6 +298,7 @@ class User(controllers.Controller): person.status = 'active' session.flush() + # TODO: Handle properly if email has already been used. This might be painful, since the person already exists, at this point. person_email = PersonEmails() person_email.email = email person_email.person = person diff --git a/fas/fas/user_email.py b/fas/fas/user_email.py index 2ef95c7..415a4cd 100644 --- a/fas/fas/user_email.py +++ b/fas/fas/user_email.py @@ -3,11 +3,16 @@ from turbogears import controllers, expose, paginate, identity, redirect, widget from turbogears.database import session import cherrypy +import turbomail +import random + from fas.model import People from fas.model import PersonEmails from fas.model import EmailPurposes from fas.model import Log +from fas.auth import * + class NonFedoraEmail(validators.FancyValidator): '''Make sure that an email address is not @fedoraproject.org''' def _to_python(self, value, state): @@ -16,13 +21,20 @@ class NonFedoraEmail(validators.FancyValidator): if value.endswith('@fedoraproject.org'): raise validators.Invalid(_("To prevent email loops, your email address cannot be @fedoraproject.org."), value, state) -class EmailCreate(validators.Schema): +class EmailSave(validators.Schema): email = validators.All( validators.Email(not_empty=True, strip=True), NonFedoraEmail(not_empty=True, strip=True), ) - #fedoraPersonBugzillaMail = validators.Email(strip=True) - postal_address = validators.String(max=512) + description = validators.String(not_empty=True, max=512) + +def generate_validtoken(length=32): + ''' Generate Validation Token ''' + chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + token = '' + for i in xrange(length): + token += random.choice(chars) + return token class Email(controllers.Controller): @@ -34,7 +46,7 @@ class Email(controllers.Controller): def index(self): '''Redirect to manage ''' - turbogears.redirect('/user/email/manage/%s' % turbogears.identity.current.user_name) + turbogears.redirect('/user/email/manage') @expose(template="fas.templates.error") @@ -48,12 +60,135 @@ class Email(controllers.Controller): #@validate(validators=UserView()) @error_handler(error) @expose(template="fas.templates.user.email.manage", allow_json=True) - def manage(self, username=None): + def manage(self, targetname=None): ''' - Manage a person's emails. + Manage a person's emails ''' - if not username: - username = turbogears.identity.current.user_name + # TODO: Some sort of auth checking - other people should + # probably be limited to looking at a person's email through + # /user/view, although admins should probably be able to set + # emails (with/without verification?) + username = turbogears.identity.current.user_name person = People.by_username(username) - return dict(person=person) + + if targetname: + target = People.by_username(targetname) + else: + target = person + + return dict(target=target) + + @identity.require(turbogears.identity.not_anonymous()) + #@validate(validators=UserView()) + @error_handler(error) + @expose(template="fas.templates.user.email.add", allow_json=True) + def add(self, targetname=None): + ''' + Display the form to add an email + ''' + username = turbogears.identity.current.user_name + person = People.by_username(username) + + if targetname: + target = People.by_username(targetname) + else: + target = person + + if not canEditUser(person, target): + turbogears.flash(_('You cannot edit %s') % target.username ) + turbogears.redirect('/user/email/manage') + return dict() + + return dict(target=target) + + @identity.require(turbogears.identity.not_anonymous()) + @validate(validators=EmailSave()) + @error_handler(error) + @expose(template="fas.templates.user.email.add", allow_json=True) + def save(self, targetname, email, description): + ''' + Display the form to add an email + ''' + username = turbogears.identity.current.user_name + person = People.by_username(username) + + if targetname: + target = People.by_username(targetname) + else: + target = person + + if not canEditUser(person, target): + turbogears.flash(_('You cannot edit %s') % target.username ) + turbogears.redirect('/user/email/manage') + return dict() + + validtoken = generate_validtoken() + + try: + person_email = PersonEmails() + person_email.email = email + person_email.person = target + person_email.description = description + person_email.validtoken = validtoken + session.flush() + # Hmm, should this be checked in the validator or here? + except IntegrityError: + turbogears.flash(_('The email \'%s\' is already in used.') % email) + return dict(target=target) + else: + # TODO: Make this email more friendly. Maybe escape the @ in email too? + validurl = config.get('base_url_filter.base_url') + turbogears.url('/user/email/verify/%s/%s/%s') % (target.username, email, validtoken) + message = turbomail.Message(config.get('accounts_mail'), email, _('Confirm this email address')) + message.plain = _(''' +Go to this URL to verify that you own this email address: %s +''') % validurl + turbomail.enqueue(message) + turbogears.flash(_('Your email has been added. Before you can use this email, you must verify it. The email you added should receive a message with instructions shortly.')) + + return dict(target=target) + + return dict(target=target) + + @identity.require(turbogears.identity.not_anonymous()) + # TODO: Validation! + #@validate(validators=UserView()) + @error_handler(error) + @expose(allow_json=True) + def verify(self, targetname, email, validtoken): + ''' + Verify an email + ''' + username = turbogears.identity.current.user_name + person = People.by_username(username) + + if targetname: + target = People.by_username(targetname) + else: + target = person + + if not canEditUser(person, target): + turbogears.flash(_('You cannot edit %s') % target.username ) + turbogears.redirect('/user/email/manage') + return dict() + + if target.person_emails[email].verified: + turbogears.flash(_('The email provided has already been verified.')) + turbogears.redirect('/user/email/manage') + return dict() + + try: + if target.person_emails[email].validtoken == validtoken: + target.person_emails[email].validtoken = '' + target.person_emails[email].verified = True + turbogears.flash(_('Your email has been successfully verified.')) + turbogears.redirect('/user/email/manage') + return dict() + else: + turbogears.flash(_('The verification string did not match.')) + turbogears.redirect('/user/email/manage') + return dict() + except KeyError: + turbogears.flash(_('No such email is associated with your user.')) + turbogears.redirect('/user/email/manage') + return dict() diff --git a/fas/fas2.sql b/fas/fas2.sql index 25dfa93..9871e40 100644 --- a/fas/fas2.sql +++ b/fas/fas2.sql @@ -71,11 +71,12 @@ cluster people_status_idx on people; CREATE TABLE person_emails ( id serial primary key, - email text not null unique, + email text not null, person_id INTEGER NOT NULL references people(id), validtoken text, description text, verified boolean NOT NULL DEFAULT false + unique (email, verified) --You can't "claim" an email before you verify it first ); create index person_emails_person_id_idx on person_emails(person_id); @@ -141,11 +142,12 @@ cluster groups_group_type_idx on groups; -- CREATE TABLE group_emails ( id serial primary key, - email text not null unique, + email text not null, group_id INTEGER NOT NULL references groups(id), validtoken text, description text, verified boolean NOT NULL DEFAULT false + unique (email, verified) --You can't "claim" an email before you verify it first ); create index group_emails_group_id_idx on group_emails(group_id);