diff options
author | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2011-06-26 18:38:06 -0400 |
---|---|---|
committer | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2011-06-26 18:38:06 -0400 |
commit | a2ca0f8234bbe231a57f75a9584054b87f777cdc (patch) | |
tree | 660f20a0d89d7a1c0f74a3766793d21d85595694 | |
parent | beb9ec2e438e74ea1cfa20f29cdb4f1d4cc18513 (diff) | |
download | askbot-a2ca0f8234bbe231a57f75a9584054b87f777cdc.tar.gz askbot-a2ca0f8234bbe231a57f75a9584054b87f777cdc.tar.bz2 askbot-a2ca0f8234bbe231a57f75a9584054b87f777cdc.zip |
added possibility to create custom login modules
-rw-r--r-- | askbot/deps/django_authopenid/backends.py | 33 | ||||
-rw-r--r-- | askbot/deps/django_authopenid/util.py | 148 | ||||
-rw-r--r-- | askbot/deps/django_authopenid/views.py | 11 | ||||
-rw-r--r-- | askbot/skins/default/templates/authopenid/macros.html | 9 | ||||
-rw-r--r-- | askbot/skins/default/templates/authopenid/signin.html | 9 | ||||
-rw-r--r-- | askbot/utils/loading.py | 16 |
6 files changed, 218 insertions, 8 deletions
diff --git a/askbot/deps/django_authopenid/backends.py b/askbot/deps/django_authopenid/backends.py index 72cc6e4f..a9eee8e5 100644 --- a/askbot/deps/django_authopenid/backends.py +++ b/askbot/deps/django_authopenid/backends.py @@ -5,6 +5,7 @@ application import datetime from django.contrib.auth.models import User from django.core.exceptions import ImproperlyConfigured +from django.utils.translation import ugettext as _ from askbot.deps.django_authopenid.models import UserAssociation from askbot.deps.django_authopenid import util @@ -47,9 +48,33 @@ class AuthBackend(object): except User.DoesNotExist: return None else: - #todo there must be a call to some sort of - #an external "check_password" function - raise NotImplementedError('do not support external passwords') + if login_providers[provider_name]['check_password'](username, password): + try: + #if have user associated with this username and provider, + #return the user + assoc = UserAssociation.objects.get( + openid_url = username + '@' + provider_name,#a hack - par name is bad + provider_name = provider_name + ) + return assoc.user + except UserAssociation.DoesNotExist: + #race condition here a user with this name may exist + user, created = User.objects.get_or_create(username = username) + if created: + user.set_password(password) + user.save() + else: + #have username collision - so make up a more unique user name + #bug: - if user already exists with the new username - we are in trouble + new_username = '%s@%s' % (username, provider_name) + user = User.objects.create_user(new_username, '', password) + message = _( + 'Welcome! Please set email address (important!) in your ' + 'profile and adjust screen name, if necessary.' + ) + user.message_set.create(message = message) + else: + return None #this is a catch - make login token a little more unique #for the cases when passwords are the same for two users @@ -64,7 +89,7 @@ class AuthBackend(object): user = user, provider_name = provider_name ) - assoc.openid_url = user.password + str(user.id) + assoc.openid_url = username + '@' + provider_name#has to be this way for external pw logins elif method == 'openid': provider_name = util.get_provider_name(openid_url) diff --git a/askbot/deps/django_authopenid/util.py b/askbot/deps/django_authopenid/util.py index e6985114..44328a25 100644 --- a/askbot/deps/django_authopenid/util.py +++ b/askbot/deps/django_authopenid/util.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- import cgi import urllib +import functools +import re from askbot.deps.openid.store.interface import OpenIDStore from askbot.deps.openid.association import Association as OIDAssociation from askbot.deps.openid.extensions import sreg @@ -31,6 +33,8 @@ from models import Association, Nonce __all__ = ['OpenID', 'DjangoOpenIDStore', 'from_openid_response', 'clean_next'] +ALLOWED_LOGIN_TYPES = ('password', 'oauth', 'openid-direct', 'openid-username') + class OpenID: def __init__(self, openid_, issued, attrs=None, sreg_=None): logging.debug('init janrain openid object') @@ -183,6 +187,144 @@ def filter_enabled_providers(data): return data +class LoginMethod(object): + """Helper class to add custom authentication modules + as plugins for the askbot's version of django_authopenid + """ + def __init__(self, login_module_path): + from askbot.utils.loading import load_module + self.mod = load_module(login_module_path) + self.mod_path = login_module_path + self.read_params() + + def get_required_attr(self, attr_name, required_for_what): + attr_value = getattr(self.mod, attr_name, None) + if attr_value is None: + raise ImproperlyConfigured( + '%s.%s is required for %s' % ( + self.mod_path, + attr_name, + required_for_what + ) + ) + return attr_value + + def read_params(self): + self.is_major = getattr(self.mod, 'BIG_BUTTON', True) + if not isinstance(self.is_major, bool): + raise ImproperlyConfigured( + 'Boolean value expected for %s.BIG_BUTTON' % self.mod_path + ) + + self.order_number = getattr(self.mod, 'ORDER_NUMBER', 1) + if not isinstance(self.order_number, int): + raise ImproperlyConfigured( + 'Integer value expected for %s.ORDER_NUMBER' % self.mod_path + ) + + self.name = getattr(self.mod, 'NAME', None) + if self.name is None or not isinstance(self.name, basestring): + raise ImproperlyConfigured( + '%s.NAME is required as a string parameter' % self.mod_path + ) + if not re.search(r'^[a-zA-Z0-9]+$', self.name): + raise ImproperlyConfigured( + '%s.NAME must be a string of ASCII letters and digits only' + ) + + self.display_name = getattr(self.mod, 'DISPLAY_NAME', None) + if self.display_name is None or not isinstance(self.display_name, basestring): + raise ImproperlyConfigured( + '%s.DISPLAY_NAME is required as a string parameter' % self.mod_path + ) + self.extra_token_name = getattr(self.mod, 'EXTRA_TOKEN_NAME', None) + self.login_type = getattr(self.mod, 'TYPE', None) + if self.login_type is None or self.login_type not in ALLOWED_LOGIN_TYPES: + raise ImproperlyConfigured( + "%s.TYPE must be a string " + "and the possible values are : 'password', 'oauth', " + "'openid-direct', 'openid-username'." % self.mod_path + ) + self.icon_media_path = getattr(self.mod, 'ICON_MEDIA_PATH', None) + if self.icon_media_path is None: + raise ImproperlyConfigured( + '%s.ICON_MEDIA_PATH is required and must be a url ' + 'to the image used as login button' % self.mod_path + ) + + self.create_password_prompt = getattr(self.mod, 'CREATE_PASSWORD_PROMPT', None) + self.change_password_prompt = getattr(self.mod, 'CHANGE_PASSWORD_PROMPT', None) + + if self.login_type == 'password': + self.check_password_function = self.get_required_attr( + 'check_password', + 'custom password login' + ) + if self.login_type == 'oauth': + for_what = 'custom OAuth login' + self.oauth_consumer_key = self.get_required_attr('OAUTH_CONSUMER_KEY', for_what) + self.oauth_consumer_secret = self.get_required_attr('OAUTH_CONSUMER_SECRET', for_what) + self.oauth_request_token_url = self.get_required_attr('OAUTH_REQUEST_TOKEN_URL', for_what) + self.oauth_access_token_url = self.get_required_attr('OAUTH_ACCESS_TOKEN_URL', for_what) + self.oauth_authorize_url = self.get_required_attr('OAUTH_AUTHORIZE_URL', for_what) + self.oauth_get_user_id_function = self.get_required_attr('oauth_get_user_id_function', for_what) + + if self.login_type.startswith('openid'): + self.openid_endpoint = self.get_required_attr('OPENID_ENDPOINT', 'custom OpenID login') + if self.login_type == 'openid-username': + if '%(username)s' not in self.openid_endpoint: + msg = 'If OpenID provider requires a username, ' + \ + 'then value of %s.OPENID_ENDPOINT must contain ' + \ + '%(username)s so that the username can be transmitted to the provider' + raise ImproperlyConfigured(msg % self.mod_path) + + self.tooltip_text = getattr(self.mod, 'TOOLTIP_TEXT', None) + + def as_dict(self): + """returns parameters as dictionary that + can be inserted into one of the provider data dictionaries + for the use in the UI""" + params = ( + 'name', 'display_name', 'type', 'icon_media_path', + 'extra_token_name', 'create_password_prompt', + 'change_password_prompt', 'consumer_key', 'consumer_secret', + 'request_token_url', 'access_token_url', 'authorize_url', + 'get_user_id_function', 'openid_endpoint', 'tooltip_text', + 'check_password', + ) + #some parameters in the class have different names from those + #in the dictionary + parameter_map = { + 'type': 'login_type', + 'consumer_key': 'oauth_consumer_key', + 'consumer_secret': 'oauth_consumer_secret', + 'request_token_url': 'oauth_request_token_url', + 'access_token_url': 'oauth_access_token_url', + 'authorize_url': 'oauth_authorize_url', + 'get_user_id_function': 'oauth_get_user_id_function', + 'check_password': 'check_password_function' + } + data = dict() + for param in params: + attr_name = parameter_map.get(param, param) + data[param] = getattr(self, attr_name, None) + if self.login_type == 'password': + #passwords in external login systems are not changeable + data['password_changeable'] = False + return data + +def add_custom_provider(func): + @functools.wraps(func) + def wrapper(): + providers = func() + login_module_path = getattr(settings, 'ASKBOT_CUSTOM_AUTH_MODULE', None) + if login_module_path: + mod = LoginMethod(login_module_path) + if mod.is_major != func.is_major: + return providers#only patch the matching provider set + providers.insert(mod.order_number - 1, mod.name, mod.as_dict()) + return providers + return wrapper def get_enabled_major_login_providers(): """returns a dictionary with data about login providers @@ -239,6 +381,7 @@ def get_enabled_major_login_providers(): 'create_password_prompt': _('Create a password-protected account'), 'change_password_prompt': _('Change your password'), 'icon_media_path': askbot_settings.LOCAL_LOGIN_ICON, + 'password_changeable': True } #if askbot_settings.FACEBOOK_KEY and askbot_settings.FACEBOOK_SECRET: @@ -268,7 +411,6 @@ def get_enabled_major_login_providers(): url = 'https://api.linkedin.com/v1/people/~:(first-name,last-name,id)' response, content = client.request(url, 'GET') if response['status'] == '200': - import re id_re = re.compile(r'<id>([^<]+)</id>') matches = id_re.search(content) if matches: @@ -319,6 +461,8 @@ def get_enabled_major_login_providers(): 'openid_endpoint': None, } return filter_enabled_providers(data) +get_enabled_major_login_providers.is_major = True +get_enabled_major_login_providers = add_custom_provider(get_enabled_major_login_providers) def get_enabled_minor_login_providers(): """same as get_enabled_major_login_providers @@ -402,6 +546,8 @@ def get_enabled_minor_login_providers(): 'openid_endpoint': 'http://%(username)s.pip.verisignlabs.com/' } return filter_enabled_providers(data) +get_enabled_minor_login_providers.is_major = False +get_enabled_minor_login_providers = add_custom_provider(get_enabled_minor_login_providers) def have_enabled_federated_login_methods(): providers = get_enabled_major_login_providers() diff --git a/askbot/deps/django_authopenid/views.py b/askbot/deps/django_authopenid/views.py index 787d7e53..c14c84f1 100644 --- a/askbot/deps/django_authopenid/views.py +++ b/askbot/deps/django_authopenid/views.py @@ -514,6 +514,17 @@ def show_signin_view( if request.user.is_authenticated(): existing_login_methods = UserAssociation.objects.filter(user = request.user) + #annotate objects with extra data + providers = util.get_enabled_login_providers() + for login_method in existing_login_methods: + provider_data = providers[login_method.provider_name] + if provider_data['type'] == 'password': + #only external password logins will not be deletable + #this is because users with those can lose access to their accounts permanently + login_method.is_deletable = provider_data.get('password_changeable', False) + else: + login_method.is_deletable = True + if view_subtype == 'default': page_title = _('Please click any of the icons below to sign in') diff --git a/askbot/skins/default/templates/authopenid/macros.html b/askbot/skins/default/templates/authopenid/macros.html index 33ef793b..477d277c 100644 --- a/askbot/skins/default/templates/authopenid/macros.html +++ b/askbot/skins/default/templates/authopenid/macros.html @@ -14,7 +14,8 @@ major_login_providers = None, minor_login_providers = None, hide_local_login = False, - settings = None + settings = None, + logged_in = False ) %} <div id="login-icons"> @@ -23,17 +24,23 @@ {% if login_provider.name == 'local' and hide_local_login == True %} {# do nothing here, left if statement this way for simplicity #} {% else %} + {% if logged_in == True and login_provider.type == 'password' and login_provider.password_changeable == False %} + {% else %} <li> {{ login_provider_input(login_provider) }} </li> + {% endif %} {% endif %} {% endfor %} </ul> <ul class="login-icons small"> {% for login_provider in minor_login_providers %} + {% if logged_in == True and login_provider.type == 'password' and login_provider.password_changeable == False %} + {% else %} <li> {{ login_provider_input(login_provider) }} </li> + {% endif %} {% endfor %} </ul> </div> diff --git a/askbot/skins/default/templates/authopenid/signin.html b/askbot/skins/default/templates/authopenid/signin.html index c6a67051..f3483bfa 100644 --- a/askbot/skins/default/templates/authopenid/signin.html +++ b/askbot/skins/default/templates/authopenid/signin.html @@ -60,7 +60,8 @@ major_login_providers = major_login_providers,
minor_login_providers = minor_login_providers,
hide_local_login = settings.SIGNIN_ALWAYS_SHOW_LOCAL_LOGIN,
- settings = settings
+ settings = settings,
+ logged_in = user.is_authenticated()
)
}}
{% if use_password_login==True %}
@@ -161,7 +162,11 @@ {% endif %}
</td>
<td>
- <button>{% trans %}delete{% endtrans %}</button>
+ {% if login_method.is_deletable %}
+ <button>{% trans %}delete{% endtrans %}</button>
+ {% else %}
+ {% trans %}cannot be deleted{% endtrans %}
+ {% endif %}
</td>
</tr>
{% endfor %}
diff --git a/askbot/utils/loading.py b/askbot/utils/loading.py new file mode 100644 index 00000000..5c57c019 --- /dev/null +++ b/askbot/utils/loading.py @@ -0,0 +1,16 @@ +"""Utilities for loading modules""" + +def load_module(mod_path): + """an equivalent of: + from some.where import module + import module + """ + assert(mod_path[0] != '.') + path_bits = mod_path.split('.') + if len(path_bits) > 1: + mod_name = path_bits.pop() + mod_prefix = '.'.join(path_bits) + _mod = __import__(mod_prefix, globals(), locals(), [mod_name,], -1) + return getattr(_mod, mod_name) + else: + return __import__(mod_path, globals(), locals(), [], -1) |