Revision 672d445a

b/snf-astakos-app/README
40 40

  
41 41
Configure in ``settings.py`` or a ``.conf`` file in ``/etc/synnefo`` if using snf-webproject.
42 42

  
43
==============================      =============================================================================   ===========================================================================================
43
=================================   =============================================================================   ===========================================================================================
44 44
Name                                Default value                                                                   Description
45
==============================      =============================================================================   ===========================================================================================
45
=================================   =============================================================================   ===========================================================================================
46 46
ASTAKOS_AUTH_TOKEN_DURATION         one month                                                                       Expiration time of newly created auth tokens
47 47
ASTAKOS_DEFAULT_USER_LEVEL          4                                                                               Default (not-invited) user level
48 48
ASTAKOS_INVITATIONS_PER_LEVEL       {0:100, 1:2, 2:0, 3:0, 4:0}                                                     Number of user invitations per user level
......
78 78
                                                                                                                    e.g. {'warning': 'Warning message (can contain html)'}
79 79
ASTAKOS_PROFILE_EXTRA_LINKS         {}                                                                               messages to display as extra actions in account forms
80 80
                                                                                                                    e.g. {'https://cms.okeanos.grnet.gr/': 'Back to ~okeanos'}
81
==============================      =============================================================================   ===========================================================================================
81
ASTAKOS_RATELIMIT_RETRIES_ALLOWED   3                                                                               Number of unsuccessful login requests allowed for a specific account.
82
                                                                                                                    When this number exceeds and ASTAKOS_RECAPTCHA_ENABLED is set the user has to solve a
83
                                                                                                                    captcha challenge.
84
=================================   =============================================================================   ===========================================================================================
82 85

  
83 86
Administrator functions
84 87
-----------------------
b/snf-astakos-app/astakos/im/activation_backends.py
208 208
        if request.method == 'POST':
209 209
            if provider == request.POST.get('provider', ''):
210 210
                initial_data = request.POST
211
        ip = self.request.META.get('REMOTE_ADDR',
212
                self.request.META.get('HTTP_X_REAL_IP', None))
213
        return globals()[formclass](initial_data, instance=instance, ip=ip)
211
        return globals()[formclass](initial_data, instance=instance, request=request)
214 212
    
215 213
    def _is_preaccepted(self, user):
216 214
        if super(SimpleBackend, self)._is_preaccepted(user):
b/snf-astakos-app/astakos/im/forms.py
76 76
        """
77 77
        Changes the order of fields, and removes the username field.
78 78
        """
79
        if 'ip' in kwargs:
80
            self.ip = kwargs['ip']
81
            kwargs.pop('ip')
79
        request = kwargs.get('request', None)
80
        if request:
81
            kwargs.pop('request')
82
            self.ip = request.META.get('REMOTE_ADDR',
83
                                       request.META.get('HTTP_X_REAL_IP', None))
84
        
82 85
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
83 86
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
84 87
                                'password1', 'password2']
......
186 189
        """
187 190
        Changes the order of fields, and removes the username field.
188 191
        """
189
        if 'ip' in kwargs:
190
            kwargs.pop('ip')
191 192
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
192 193
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
193 194
                                'provider', 'third_party_identifier']
......
284 285
    
285 286
class LoginForm(AuthenticationForm):
286 287
    username = forms.EmailField(label=_("Email"))
288
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
289
    recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
290
    
291
    def __init__(self, *args, **kwargs):
292
        was_limited = kwargs.get('was_limited', False)
293
        request = kwargs.get('request', None)
294
        if request:
295
            self.ip = request.META.get('REMOTE_ADDR',
296
                                       request.META.get('HTTP_X_REAL_IP', None))
297
        
298
        t = ('request', 'was_limited')
299
        for elem in t:
300
            if elem in kwargs.keys():
301
                kwargs.pop(elem)
302
        super(LoginForm, self).__init__(*args, **kwargs)
303
        
304
        self.fields.keyOrder = ['username', 'password']
305
        if was_limited and RECAPTCHA_ENABLED:
306
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
307
                                         'recaptcha_response_field',])
308
    
309
    def clean_recaptcha_response_field(self):
310
        if 'recaptcha_challenge_field' in self.cleaned_data:
311
            self.validate_captcha()
312
        return self.cleaned_data['recaptcha_response_field']
313

  
314
    def clean_recaptcha_challenge_field(self):
315
        if 'recaptcha_response_field' in self.cleaned_data:
316
            self.validate_captcha()
317
        return self.cleaned_data['recaptcha_challenge_field']
318

  
319
    def validate_captcha(self):
320
        rcf = self.cleaned_data['recaptcha_challenge_field']
321
        rrf = self.cleaned_data['recaptcha_response_field']
322
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
323
        if not check.is_valid:
324
            raise forms.ValidationError(_('You have not entered the correct words'))
287 325

  
288 326
class ProfileForm(forms.ModelForm):
289 327
    """
b/snf-astakos-app/astakos/im/settings.py
80 80
# e.g. {'https://cms.okeanos.grnet.gr/': 'Back to ~okeanos'}
81 81
PROFILE_EXTRA_LINKS = getattr(settings, 'ASTAKOS_PROFILE_EXTRA_LINKS', {})
82 82

  
83
# The number of unsuccessful login requests per minute allowed for a specific email
84
RATELIMIT_RETRIES_ALLOWED = getattr(settings, 'ASTAKOS_RATELIMIT_RETRIES_ALLOWED', 3)
85

  
b/snf-astakos-app/astakos/im/target/local.py
42 42
from astakos.im.views import requires_anonymous
43 43
from astakos.im.models import AstakosUser
44 44
from astakos.im.forms import LoginForm
45
from astakos.im.settings import RATELIMIT_RETRIES_ALLOWED
46

  
47
from ratelimit.decorators import ratelimit
48

  
49
retries = RATELIMIT_RETRIES_ALLOWED-1
50
rate = str(retries)+'/m'
45 51

  
46 52
@requires_anonymous
53
@ratelimit(field='username', method='POST', rate=rate)
47 54
def login(request, on_failure='im/login.html'):
48 55
    """
49 56
    on_failure: the template name to render on login failure
50 57
    """
51
    form = LoginForm(data=request.POST)
58
    was_limited = getattr(request, 'limited', False)
59
    form = LoginForm(data=request.POST, was_limited=was_limited, request=request)
52 60
    next = request.POST.get('next')
53 61
    if not form.is_valid():
54 62
        return render_to_response(on_failure,
b/snf-astakos-app/astakos/im/target/shibboleth.py
84 84
            message = _('Inactive account')
85 85
            messages.add_message(request, messages.ERROR, message)
86 86
            return render_response(on_login_template,
87
                                   login_form = LoginForm(),
87
                                   login_form = LoginForm(request=request),
88 88
                                   context_instance=RequestContext(request))
89 89
    except AstakosUser.DoesNotExist, e:
90 90
        user = AstakosUser(third_party_identifier=eppn, realname=realname,
b/snf-astakos-app/astakos/im/views.py
135 135
    if request.user.is_authenticated():
136 136
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
137 137
    return render_response(template_name,
138
                           login_form = LoginForm(),
138
                           login_form = LoginForm(request=request),
139 139
                           context_instance = get_context(request, extra_context))
140 140

  
141 141
@login_required
b/snf-astakos-app/setup.py
79 79
    'South>=0.7, <=0.7.3',
80 80
    'httplib2>=0.6.0',
81 81
    'snf-common>=0.9.0',
82
    'recaptcha-client>=1.0.5'
82
    'recaptcha-client>=1.0.5',
83
    'django-ratelimit==0.1'
83 84
]
84 85

  
85 86
EXTRAS_REQUIRES = {

Also available in: Unified diff