Please excuse my ignorance on this as I am brand new to programming and web2py. For this issue what necessitates the need to store the bare username instead of potentially storing the full email or UPN? I know in our environment the sAMAccountName is not necessarily unique across the forest but the UPN is. In intial testing I have commented out line 255 from ldap_auth.py to resolve this issue but I am not sure what the full implications of that are elsewhere in the Auth module.
On Thursday, August 22, 2013 1:16:48 PM UTC-4, Massimo Di Pierro wrote: > > i agree > > On Thursday, 22 August 2013 11:16:20 UTC-5, Richard wrote: >> >> Hello, >> >> I just push these change in production, then face some exception... Some >> of our users have email as username some don't... So the fix I found is not >> the proper solution... I think we will have to improve ldap_auth logic to >> get out of the mud with this... I will look throught this a bit more see >> what I can come up with. What ldap_auth should do is to check user >> credentials, if the user log with email but he already have a user with the >> same username and password it should use this user instead of creating a >> new user with email as username and missing first_name and many other field >> empty... >> >> Richard >> >> >> On Wed, Aug 21, 2013 at 9:34 AM, Richard Vézina <[email protected]>wrote: >> >>> No problem, thank for the follow... >>> >>> Richard >>> >>> >>> On Wed, Aug 21, 2013 at 4:51 AM, Massimo Di Pierro < >>> [email protected]> wrote: >>> >>>> I have not forgotten. I opened issue 1645. >>>> >>>> >>>> On Tuesday, 13 August 2013 15:13:30 UTC-5, Richard wrote: >>>> >>>>> Massimo, >>>>> >>>>> If you are concern about possible backward compatibility issue that >>>>> this change could raise... Maybe we could find a way to let the ldap_auth >>>>> return validator error to form (to user)... I could live with that too... >>>>> I >>>>> just didn't find a easy way to make it works from ldap_auth (mean a lot >>>>> of >>>>> refactoring could be required and I don't want to screw up ldap_auth for >>>>> others, I am not equiped to test it properly over different ldap >>>>> implementation). I just have to change a single line in tools.py + add a >>>>> IS_NOT_EMAIL() validator, that was the easiest... >>>>> >>>>> :) >>>>> >>>>> Richard >>>>> >>>>> >>>>> On Tue, Aug 13, 2013 at 10:19 AM, Richard Vézina < >>>>> [email protected]> wrote: >>>>> >>>>>> No problem! >>>>>> >>>>>> As long as there is a solution in the next version I will be happy... >>>>>> Curious to know what is bugging you though, adding a new validator? >>>>>> Maybe >>>>>> the IS_MAIL() could be hack in order that it usage could be reversed, >>>>>> something like this : IS_MAIL(..., complement=True), then it will return >>>>>> true if the string is not a email... So don't need a new validator. >>>>>> >>>>>> Richard >>>>>> >>>>>> >>>>>> >>>>>> >>>>>> On Tue, Aug 13, 2013 at 10:04 AM, Massimo Di Pierro < >>>>>> [email protected]> wrote: >>>>>> >>>>>>> I need to review this. I understand the problem but I am not >>>>>>> convinced by the solution. I will need one week. >>>>>>> >>>>>>> >>>>>>> On Monday, 12 August 2013 08:46:31 UTC-5, Richard wrote: >>>>>>> >>>>>>>> UP! >>>>>>>> >>>>>>>> >>>>>>>> >>>>>>>> On Fri, Aug 9, 2013 at 10:59 AM, Richard Vézina < >>>>>>>> [email protected]> wrote: >>>>>>>> >>>>>>>>> Here the patch!! >>>>>>>>> >>>>>>>>> I have not been able to use the IS_NOT_EMAIL() validator from >>>>>>>>> validators.py didn't understand how validators are import in >>>>>>>>> tools.py... >>>>>>>>> >>>>>>>>> NOTE : >>>>>>>>> About my precedent mail... the "auth_table.username.requires = >>>>>>>>> IS_NOT_IN_DB(db, auth_table.username" from the book is not require if >>>>>>>>> the >>>>>>>>> user use the "auth.define_tables(username=**T**rue)" and the >>>>>>>>> recommended auth_tables customization mechanism. So, I think the book >>>>>>>>> should be revised this way : >>>>>>>>> >>>>>>>>> "In case you use old style customizing auth_tables. Make sure your >>>>>>>>> username field definition looks like that : >>>>>>>>> Field('username', 'string', >>>>>>>>> notnull=True, >>>>>>>>> required=True, >>>>>>>>> requires=[IS_NOT_EMPTY(error_**m** >>>>>>>>> essage=T(auth.messages.is_**empt**y)), >>>>>>>>> IS_NOT_IN_DB(db, 'auth_user.username'), >>>>>>>>> IS_NOT_EMAIL()] >>>>>>>>> ), >>>>>>>>> >>>>>>>>> Where you make sure you use these validators in order to make sure >>>>>>>>> email is not used as username and there is no duplicated username in >>>>>>>>> your >>>>>>>>> auth_user table." >>>>>>>>> >>>>>>>>> :) >>>>>>>>> >>>>>>>>> Richard >>>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>>> On Wed, Aug 7, 2013 at 1:48 PM, Richard Vézina < >>>>>>>>> [email protected]> wrote: >>>>>>>>> >>>>>>>>>> I would also add this : >>>>>>>>>> >>>>>>>>>> tmpvalidator = >>>>>>>>>> [IS_NOT_EMPTY(error_message=se****lf.messages.is_empty), >>>>>>>>>> IS_NOT_IN_DB(db, 'auth_user.username'), IS_NOT_EMAIL()] >>>>>>>>>> >>>>>>>>>> Since this line in the book : >>>>>>>>>> >>>>>>>>>> auth_table.username.requires = IS_NOT_IN_DB(db, auth_table.username) >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> Doesn't seem to works and could be erase since who want a >>>>>>>>>> duplicated username in his auth_user?? >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> Richard >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> On Wed, Aug 7, 2013 at 1:35 PM, Richard Vézina < >>>>>>>>>> [email protected]> wrote: >>>>>>>>>> >>>>>>>>>>> Better then that I think : >>>>>>>>>>> >>>>>>>>>>> In gluon/tools.py in Auth login() near line 2006 : >>>>>>>>>>> >>>>>>>>>>> Replace : >>>>>>>>>>> tmpvalidator = IS_NOT_EMPTY(error_message=**sel** >>>>>>>>>>> f.messages.is_empty) >>>>>>>>>>> >>>>>>>>>>> With : >>>>>>>>>>> if 'username' in table_user.fields or \ >>>>>>>>>>> not self.settings.login_email_**vali**date: >>>>>>>>>>> tmpvalidator = >>>>>>>>>>> [IS_NOT_EMPTY(error_message=**se**lf.messages.is_empty), >>>>>>>>>>> IS_NOT_EMAIL()] >>>>>>>>>>> >>>>>>>>>>> Will require to add this new validator though : >>>>>>>>>>> >>>>>>>>>>> class IS_NOT_EMAIL: >>>>>>>>>>> def __init__(self, error_message='You can\'t use email as >>>>>>>>>>> username'): >>>>>>>>>>> self.e = error_message >>>>>>>>>>> def __call__(self, value): >>>>>>>>>>> if not IS_EMAIL()(value)[1]: >>>>>>>>>>> return (value, self.e) >>>>>>>>>>> return (value, None) >>>>>>>>>>> >>>>>>>>>>> What you think about that?? >>>>>>>>>>> >>>>>>>>>>> Richard >>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> On Wed, Aug 7, 2013 at 1:23 PM, Richard Vézina < >>>>>>>>>>> [email protected]> wrote: >>>>>>>>>>> >>>>>>>>>>>> from gluon.validators import IS_EMAIL >>>>>>>>>>>> >>>>>>>>>>>> if ldap_mode == 'ad': >>>>>>>>>>>> # Microsoft Active Directory >>>>>>>>>>>> if IS_EMAIL()(username)[1] is not None: >>>>>>>>>>>> #if '@' not in username: >>>>>>>>>>>> domain = [] >>>>>>>>>>>> for x in ldap_basedn.split(','): >>>>>>>>>>>> if "DC=" in x.upper(): >>>>>>>>>>>> domain.append(x.split('=')[-1]****) >>>>>>>>>>>> username = "%s@%s" % (username, >>>>>>>>>>>> '.'.join(domain)) >>>>>>>>>>>> else: >>>>>>>>>>>> return False >>>>>>>>>>>> username_bare = username.split("@")[0] >>>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>>> This prevent login to occure and new user to be inserted when >>>>>>>>>>>> email is used as username... however it not returning any advise >>>>>>>>>>>> to the >>>>>>>>>>>> user... I will try to figure out how to implement validation from >>>>>>>>>>>> ldap_auth >>>>>>>>>>>> and get back with a patch. >>>>>>>>>>>> >>>>>>>>>>>> Richard >>>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>>> On Wed, Aug 7, 2013 at 10:56 AM, Richard Vézina < >>>>>>>>>>>> [email protected]> wrote: >>>>>>>>>>>> >>>>>>>>>>>>> Ok! >>>>>>>>>>>>> >>>>>>>>>>>>> :) >>>>>>>>>>>>> >>>>>>>>>>>>> Richard >>>>>>>>>>>>> >>>>>>>>>>>>> >>>>>>>>>>>>> On Wed, Aug 7, 2013 at 10:10 AM, Massimo Di Pierro < >>>>>>>>>>>>> [email protected]> wrote: >>>>>>>>>>>>> >>>>>>>>>>>>>> Please send me a patch when you test it. ;-) >>>>>>>>>>>>>> >>>>>>>>>>>>>> On Wednesday, 7 August 2013 07:51:58 UTC-5, Richard wrote: >>>>>>>>>>>>>> >>>>>>>>>>>>>>> No change... Auth seems to delegate entirely the validation >>>>>>>>>>>>>>> on username input field in case ldap_auth is used as >>>>>>>>>>>>>>> authentication method. >>>>>>>>>>>>>>> >>>>>>>>>>>>>>> I guess this simple refactor (not tested) could do the >>>>>>>>>>>>>>> tricks at least for Active directory : >>>>>>>>>>>>>>> >>>>>>>>>>>>>>> if not IS_EMAIL()(username)[1]: >>>>>>>>>>>>>>> domain = [] >>>>>>>>>>>>>>> for x in ldap_basedn.split(','): >>>>>>>>>>>>>>> if "DC=" in x.upper(): >>>>>>>>>>>>>>> domain.append(x.split('=')[-1]** >>>>>>>>>>>>>>> ****) >>>>>>>>>>>>>>> identifier = "%s@%s" % (username, >>>>>>>>>>>>>>> '.'.join(domain)) >>>>>>>>>>>>>>> else: return ERROR... >>>>>>>>>>>>>>> username_bare = username.split("@")[0] >>>>>>>>>>>>>>> >>>>>>>>>>>>>>> Richard >>>>>>>>>>>>>>> >>>>>>>>>>>>>>> >>>>>>>>>>>>>>> >>>>>>>>>>>>>>> On Wed, Aug 7, 2013 at 8:42 AM, Richard Vézina < >>>>>>>>>>>>>>> [email protected]> wrote: >>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> Hello, >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> I was about to post this (I think I answer your question) : >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> Hello, >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> I think I found a flaw in the interaction between Auth and >>>>>>>>>>>>>>>> LDAP contrib (web2py 2.4.7). >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> If I set LDAP as unique authentification method >>>>>>>>>>>>>>>> (auth.settings.login_methods = LDAP) as written in the book, >>>>>>>>>>>>>>>> web2py should >>>>>>>>>>>>>>>> leaves LDAP to create user... The things is web2py Auth seems >>>>>>>>>>>>>>>> to create >>>>>>>>>>>>>>>> user even if it LDAP that is responsible of doing it. I mean, >>>>>>>>>>>>>>>> I carefully >>>>>>>>>>>>>>>> read the code of LDAP and the only way it could create a new >>>>>>>>>>>>>>>> user is if >>>>>>>>>>>>>>>> manage_groups=True by calling do_manage_groups() since the >>>>>>>>>>>>>>>> other place >>>>>>>>>>>>>>>> where LDAP is instert new user it set email, first_name and >>>>>>>>>>>>>>>> last_name. In >>>>>>>>>>>>>>>> my case, if user use email instead of username (that should >>>>>>>>>>>>>>>> not be email, >>>>>>>>>>>>>>>> but I can't enforce this with the custom IS_NOT_EMAIL() >>>>>>>>>>>>>>>> validator I wrote) >>>>>>>>>>>>>>>> for login a new user get inserted like this : first_name = >>>>>>>>>>>>>>>> email (or the >>>>>>>>>>>>>>>> content of username input that is an email), username = email >>>>>>>>>>>>>>>> and >>>>>>>>>>>>>>>> registration_id = email. As far as I can see the only way LDAP >>>>>>>>>>>>>>>> could >>>>>>>>>>>>>>>> produce this result is if the do_manage_groups method is >>>>>>>>>>>>>>>> called, but it >>>>>>>>>>>>>>>> can't be call if manage_groups is set to False. So, the only >>>>>>>>>>>>>>>> remaining >>>>>>>>>>>>>>>> possibility is that Auth is creating the new user because it >>>>>>>>>>>>>>>> recieve a bad >>>>>>>>>>>>>>>> signal from LDAP. >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> I make a couples tests and found that the insert new user >>>>>>>>>>>>>>>> base on the credentials of already existing user that log with >>>>>>>>>>>>>>>> it email >>>>>>>>>>>>>>>> instead of it username occure at line 2147-2148. So I guess >>>>>>>>>>>>>>>> Auth recieve a >>>>>>>>>>>>>>>> True flag from LDAP mean the user exist in directory, since >>>>>>>>>>>>>>>> web2py can't >>>>>>>>>>>>>>>> match a existing user base on the wrong username (email) it >>>>>>>>>>>>>>>> insert a new >>>>>>>>>>>>>>>> user with wrong setting. >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> The origin of this is multifold. First, I think it could be >>>>>>>>>>>>>>>> prevent if there was a IS_NOT_EMAIL() validator on the >>>>>>>>>>>>>>>> username field, for >>>>>>>>>>>>>>>> some reason I can't get it to work properly with LDAP because >>>>>>>>>>>>>>>> of the way >>>>>>>>>>>>>>>> LDAP is working the validator seems to be skipped, and the >>>>>>>>>>>>>>>> username is >>>>>>>>>>>>>>>> first check against directory. Maybe using IS_NOT_EMAIL() >>>>>>>>>>>>>>>> inside ldap_auth >>>>>>>>>>>>>>>> contrib could solve this issue. Other possible origin is the >>>>>>>>>>>>>>>> way ldap_auth >>>>>>>>>>>>>>>> is written. I mean it seems that for saving a variable >>>>>>>>>>>>>>>> "username" is >>>>>>>>>>>>>>>> re-used... I think that the issue is coming from line 8 of >>>>>>>>>>>>>>>> code extract >>>>>>>>>>>>>>>> below : >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> if ldap_mode == 'ad': >>>>>>>>>>>>>>>> # Microsoft Active Directory >>>>>>>>>>>>>>>> if '@' not in username: >>>>>>>>>>>>>>>> domain = [] >>>>>>>>>>>>>>>> for x in ldap_basedn.split(','): >>>>>>>>>>>>>>>> if "DC=" in x.upper(): >>>>>>>>>>>>>>>> domain.append(x.split('=')[-1]* >>>>>>>>>>>>>>>> *****) >>>>>>>>>>>>>>>> username = "%s@%s" % (username, >>>>>>>>>>>>>>>> '.'.join(domain)) >>>>>>>>>>>>>>>> username_bare = username.split("@")[0] >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> con.set_option(ldap.OPT_**PROTOC****OL_VERSION, >>>>>>>>>>>>>>>> 3) >>>>>>>>>>>>>>>> # In cases where ForestDnsZones and >>>>>>>>>>>>>>>> DomainDnsZones are found, >>>>>>>>>>>>>>>> # result will look like the following: >>>>>>>>>>>>>>>> # ['ldap://ForestDnsZones.**domain**** >>>>>>>>>>>>>>>> .com/DC=ForestDnsZones<http://ForestDnsZones.domain.com/DC=ForestDnsZones> >>>>>>>>>>>>>>>> , >>>>>>>>>>>>>>>> # DC=domain,DC=com'] >>>>>>>>>>>>>>>> if ldap_binddn: >>>>>>>>>>>>>>>> # need to search directory with an >>>>>>>>>>>>>>>> admin account 1st >>>>>>>>>>>>>>>> con.simple_bind_s(ldap_binddn, >>>>>>>>>>>>>>>> ldap_bindpw) >>>>>>>>>>>>>>>> else: >>>>>>>>>>>>>>>> # credentials should be in the form of >>>>>>>>>>>>>>>> [email protected] >>>>>>>>>>>>>>>> con.simple_bind_s(username, password) >>>>>>>>>>>>>>>> # this will throw an index error if the >>>>>>>>>>>>>>>> account is not found >>>>>>>>>>>>>>>> # in the ldap_basedn >>>>>>>>>>>>>>>> requested_attrs = ['sAMAccountName'] >>>>>>>>>>>>>>>> if manage_user: >>>>>>>>>>>>>>>> requested_attrs.extend([user_**f**** >>>>>>>>>>>>>>>> irstname_attrib, >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> user_lastname_attrib, >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> user_mail_attrib]) >>>>>>>>>>>>>>>> result = con.search_ext_s( >>>>>>>>>>>>>>>> ldap_basedn, ldap.SCOPE_SUBTREE, >>>>>>>>>>>>>>>> "(&(sAMAccountName=%s)(%s))" % ( >>>>>>>>>>>>>>>> ldap.filter.escape_filter_* >>>>>>>>>>>>>>>> *char****s(username_bare), >>>>>>>>>>>>>>>> filterstr), >>>>>>>>>>>>>>>> requested_attrs)[0][1] >>>>>>>>>>>>>>>> if not isinstance(result, dict): >>>>>>>>>>>>>>>> # result should be a dict in the form >>>>>>>>>>>>>>>> # {'sAMAccountName': [username_bare]} >>>>>>>>>>>>>>>> logger.warning('User [%s] not found!' % >>>>>>>>>>>>>>>> username) >>>>>>>>>>>>>>>> return False >>>>>>>>>>>>>>>> if ldap_binddn: >>>>>>>>>>>>>>>> # We know the user exists & is in the >>>>>>>>>>>>>>>> correct OU >>>>>>>>>>>>>>>> # so now we just check the password >>>>>>>>>>>>>>>> con.simple_bind_s(username, password) >>>>>>>>>>>>>>>> username = username_bare >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> This peace of code is pretty unreliable : It start by >>>>>>>>>>>>>>>> re-creating a email and store it in username vars if username >>>>>>>>>>>>>>>> it recieves >>>>>>>>>>>>>>>> from web2py is not a email before derive a username_bare from >>>>>>>>>>>>>>>> the altered >>>>>>>>>>>>>>>> username var and at the end it finally set username = >>>>>>>>>>>>>>>> username_bare... Why >>>>>>>>>>>>>>>> all this just to avoid create a var?! >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> I propose to refator this using creating a new ID or >>>>>>>>>>>>>>>> identifier var to store connection "identifier" var instead >>>>>>>>>>>>>>>> reusing the >>>>>>>>>>>>>>>> username for that. Then it will require to determine if the >>>>>>>>>>>>>>>> IS_NOT_EMAIL() >>>>>>>>>>>>>>>> should go at Auth level or ldap_auth. I don't know so much >>>>>>>>>>>>>>>> LDAP in general >>>>>>>>>>>>>>>> and even less the different implementation, so I don't know if >>>>>>>>>>>>>>>> some >>>>>>>>>>>>>>>> implementation use email as an identifier or not. Since, the >>>>>>>>>>>>>>>> Auth class as >>>>>>>>>>>>>>>> mechanism to create missing user I don't no if it intentional >>>>>>>>>>>>>>>> to allow the >>>>>>>>>>>>>>>> creation of user with email as username or not... So, maybe it >>>>>>>>>>>>>>>> a option in >>>>>>>>>>>>>>>> to use IS_NOT_EMAIL() on username field in this case it will >>>>>>>>>>>>>>>> require that >>>>>>>>>>>>>>>> IS_NOT_EMAIL be at level of Auth. Maybe, I didn't be able to >>>>>>>>>>>>>>>> make work my >>>>>>>>>>>>>>>> custom validator because of the order of validator (I had set >>>>>>>>>>>>>>>> multiple >>>>>>>>>>>>>>>> validator on username), I will try to set only IS_NOT_EMAIL >>>>>>>>>>>>>>>> and report here >>>>>>>>>>>>>>>> if it solve the problem I have with LDAP authentication. >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> Thanks >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> Richard >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> On Wed, Aug 7, 2013 at 7:22 AM, Massimo Di Pierro < >>>>>>>>>>>>>>>> [email protected]> wrote: >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>> How would you change this? >>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>> On Monday, 5 August 2013 15:42:39 UTC-5, Richard wrote: >>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>>> Hello, >>>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>>> Is there a way to prevent user to log with there email? I >>>>>>>>>>>>>>>>>> set LDAP authentication, I create a username field on custom >>>>>>>>>>>>>>>>>> auth_user >>>>>>>>>>>>>>>>>> model and set auth.define_tables(username=**Tr******ue) >>>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>>> But I notice that I can still login with [email protected]. >>>>>>>>>>>>>>>>>> In this case, ldap_auth create a new user with first_name >>>>>>>>>>>>>>>>>> and username = >>>>>>>>>>>>>>>>>> [email protected] >>>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>>> So, I think there is a flaw here in ldap_auth : >>>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>>> if ldap_mode == 'ad': >>>>>>>>>>>>>>>>>> # Microsoft Active Directory >>>>>>>>>>>>>>>>>> if '@' not in username: >>>>>>>>>>>>>>>>>> domain = [] >>>>>>>>>>>>>>>>>> for x in ldap_basedn.split(','): >>>>>>>>>>>>>>>>>> if "DC=" in x.upper(): >>>>>>>>>>>>>>>>>> domain.append(x.split('=')[-1] >>>>>>>>>>>>>>>>>> ********) >>>>>>>>>>>>>>>>>> username = "%s@%s" % (username, >>>>>>>>>>>>>>>>>> '.'.join(domain)) >>>>>>>>>>>>>>>>>> username_bare = username.split("@")[0] >>>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>>> Since it seems to recreate email as username... >>>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>>> Thanks >>>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>>> Richard >>>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>> -- >>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>> --- >>>>>>>>>>>>>>>>> You received this message because you are subscribed to >>>>>>>>>>>>>>>>> the Google Groups "web2py-users" group. >>>>>>>>>>>>>>>>> To unsubscribe from this group and stop receiving emails >>>>>>>>>>>>>>>>> from it, send an email to web2py+un...@**googlegroups.com. >>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>> For more options, visit https://groups.google.com/**grou** >>>>>>>>>>>>>>>>> **ps/opt_out <https://groups.google.com/groups/opt_out>. >>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>>> >>>>>>>>>>>>>>> -- >>>>>>>>>>>>>> >>>>>>>>>>>>>> --- >>>>>>>>>>>>>> You received this message because you are subscribed to the >>>>>>>>>>>>>> Google Groups "web2py-users" group. >>>>>>>>>>>>>> To unsubscribe from this group and stop receiving emails from >>>>>>>>>>>>>> it, send an email to web2py+un...@**googlegroups.com. >>>>>>>>>>>>>> For more options, visit https://groups.google.com/**grou** >>>>>>>>>>>>>> ps/opt_out <https://groups.google.com/groups/opt_out>. >>>>>>>>>>>>>> >>>>>>>>>>>>>> >>>>>>>>>>>>>> >>>>>>>>>>>>> >>>>>>>>>>>>> >>>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>> >>>>>>>>> >>>>>>>> -- >>>>>>> >>>>>>> --- >>>>>>> You received this message because you are subscribed to the Google >>>>>>> Groups "web2py-users" group. >>>>>>> To unsubscribe from this group and stop receiving emails from it, >>>>>>> send an email to web2py+un...@**googlegroups.com. >>>>>>> For more options, visit >>>>>>> https://groups.google.com/**groups/opt_out<https://groups.google.com/groups/opt_out> >>>>>>> . >>>>>>> >>>>>>> >>>>>>> >>>>>> >>>>>> >>>>> -- >>>> >>>> --- >>>> You received this message because you are subscribed to the Google >>>> Groups "web2py-users" group. >>>> To unsubscribe from this group and stop receiving emails from it, send >>>> an email to [email protected]. >>>> For more options, visit https://groups.google.com/groups/opt_out. >>>> >>> >>> >> -- --- You received this message because you are subscribed to the Google Groups "web2py-users" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. For more options, visit https://groups.google.com/groups/opt_out.

