Browse Source

fix #518: secret has to be known now to access avatar source

Rafał Pitoń 10 years ago
parent
commit
ec1080f99a

+ 16 - 9
misago/emberapp/app/components/forms/avatar-crop-form.js

@@ -7,9 +7,9 @@ export default Ember.Component.extend({
   isLoading: true,
   isCropping: false,
 
-  suffix: 'org',
-  isNested: Ember.computed.equal('suffix', 'tmp'),
-  token: null,
+  secret: '',
+  isNested: false,
+  hash: null,
 
   crop: null,
 
@@ -17,9 +17,13 @@ export default Ember.Component.extend({
     return 'users/' + this.auth.get('user.id') + '/avatar';
   }.property(),
 
-  finalToken: function() {
-    return this.get('token') || this.get('options.crop_org.token');
-  }.property('token', 'options.crop_org'),
+  finalSecret: function() {
+    return this.get('secret') || this.get('options.crop_org.secret');
+  }.property('secret', 'options.crop_org.secret'),
+
+  finalHash: function() {
+    return this.get('hash') || this.get('auth.user.avatar_hash');
+  }.property('hash', 'auth.user.avatar_hash'),
 
   avatarSize: function() {
     if (this.get('isNested')) {
@@ -31,10 +35,10 @@ export default Ember.Component.extend({
 
   imagePath: function() {
     var src = Ember.$('base').attr('href') + 'user-avatar/';
-    src += this.get('suffix') + ':' + this.get('finalToken') + '/';
+    src += this.get('finalSecret') + ':' + this.get('finalHash') + '/';
     src += this.get('auth.user.id') + '.png';
     return src;
-  }.property('suffix', 'finalToken', 'auth.user.id'),
+  }.property('finalSecret', 'finalHash', 'auth.user.id'),
 
   loadLibrary: function() {
     var self = this;
@@ -91,7 +95,10 @@ export default Ember.Component.extend({
       if (this.get('isCropping')) { return; }
       this.set('isCropping', true);
 
-      var opName = 'crop_' + this.get('suffix');
+      var opName = 'crop_org';
+      if (this.get('isNested')) {
+        opName = 'crop_tmp';
+      }
 
       var $cropper = this.$('.image-cropper');
 

+ 2 - 2
misago/emberapp/app/components/forms/avatar-upload-form.js

@@ -9,7 +9,7 @@ export default Ember.Component.extend({
   selectedImage: null,
   isUploading: false,
   progress: 0,
-  uploadToken: null,
+  uploadHash: null,
 
   apiUrl: function() {
     return 'users/' + this.auth.get('user.id') + '/avatar';
@@ -90,7 +90,7 @@ export default Ember.Component.extend({
     }).then(function(data) {
       if (self.isDestroyed) { return; }
       self.toast.info(gettext("Your image was uploaded successfully."));
-      self.set('uploadToken', data.detail);
+      self.set('uploadHash', data.detail);
       self.get('options').setProperties(data.options);
     }, function(jhXHR) {
       if (self.isDestroyed) { return; }

+ 3 - 3
misago/emberapp/app/templates/components/forms/avatar-upload-form.hbs

@@ -1,5 +1,5 @@
-{{#if uploadToken}}
-  {{avatar-crop-form options=options activeForm=activeForm suffix="tmp" token=uploadToken}}
+{{#if uploadHash}}
+  {{avatar-crop-form options=options activeForm=activeForm isNested=true secret=options.crop_tmp.secret hash=uploadHash}}
 {{/if}}
 
 <input class="file-upload" type="file">
@@ -18,7 +18,7 @@
 </div>
 {{/if}}
 
-{{#unless uploadToken}}
+{{#unless uploadHash}}
 <div class="row">
   <div class="col-md-6 col-md-offset-3">
 

+ 5 - 1
misago/users/api/userendpoints/avatar.py

@@ -54,11 +54,14 @@ def get_avatar_options(user):
     # Allow Gravatar download
     options['gravatar'] = True
 
+    # Get avatar tokens
+    tokens = avatars.get_user_avatar_tokens(user)
+
     # Allow crop with token if we have uploaded avatar
     if avatars.uploaded.has_original_avatar(user):
         try:
             options['crop_org'] = {
-                'token': avatars.get_avatar_hash(user, 'org'),
+                'secret': tokens['org'],
                 'crop': json.loads(user.avatar_crop),
                 'size': max(settings.MISAGO_AVATARS_SIZES)
             }
@@ -68,6 +71,7 @@ def get_avatar_options(user):
     # Allow crop of uploaded avatar
     if avatars.uploaded.has_temporary_avatar(user):
         options['crop_tmp'] = {
+            'secret': tokens['tmp'],
             'size': max(settings.MISAGO_AVATARS_SIZES)
         }
 

+ 1 - 0
misago/users/avatars/__init__.py

@@ -25,3 +25,4 @@ def set_default_avatar(user):
 
 get_avatar_hash = store.get_avatar_hash
 delete_avatar = store.delete_avatar
+get_user_avatar_tokens = store.get_user_avatar_tokens

+ 16 - 0
misago/users/avatars/store.py

@@ -56,6 +56,22 @@ def delete_avatar(user):
             avatar_file.remove()
 
 
+def get_user_avatar_tokens(user):
+    token_seeds = (user.email, user.avatar_hash, settings.SECRET_KEY)
+
+    tokens = {
+        'org': md5('org:%s:%s:%s' % token_seeds).hexdigest()[:8],
+        'tmp': md5('tmp:%s:%s:%s' % token_seeds).hexdigest()[:8],
+    }
+
+    tokens.update({
+        tokens['org']: 'org',
+        tokens['tmp']: 'tmp',
+    })
+
+    return tokens
+
+
 def store_temporary_avatar(user, image):
     avatars_dir = get_existing_avatars_dir(user)
     avatar_file = '%s_tmp.png' % user.pk

+ 5 - 0
misago/users/tests/test_avatars.py

@@ -45,6 +45,11 @@ class AvatarsStoreTests(TestCase):
         self.assertEqual(len(test_user.avatar_hash), 8)
         test_user.save(update_fields=['avatar_hash'])
 
+        # Get avatar tokens
+        tokens = store.get_user_avatar_tokens(test_user)
+        self.assertEqual(tokens[tokens['org']], 'org')
+        self.assertEqual(tokens[tokens['tmp']], 'tmp')
+
         # Delete avatar
         store.delete_avatar(test_user)
         for size in settings.MISAGO_AVATARS_SIZES:

+ 22 - 0
misago/users/tests/test_user_avatar_api.py

@@ -2,6 +2,7 @@ import json
 from path import Path
 
 from django.contrib.auth import get_user_model
+from django.core.urlresolvers import reverse
 
 from misago.conf import settings
 
@@ -126,6 +127,16 @@ class UserAvatarTests(AuthenticatedUserTestCase):
             self.assertTrue(avatar.exists())
             self.assertTrue(avatar.isfile())
 
+            tmp_avatar_kwargs = {
+                'secret': response_json['options']['crop_tmp']['secret'],
+                'hash': response_json['avatar_hash'],
+                'user_id': self.user.pk
+            }
+            tmp_avatar_path = reverse('misago:user_avatar_source',
+                                      kwargs=tmp_avatar_kwargs)
+            response = self.client.get(tmp_avatar_path)
+            self.assertEqual(response.status_code, 200)
+
             response = self.client.post(self.link, json.dumps({
                     'avatar': 'crop_tmp',
                     'crop': {
@@ -136,6 +147,7 @@ class UserAvatarTests(AuthenticatedUserTestCase):
                     }
                 }),
                 content_type="application/json")
+            response_json = json.loads(response.content)
 
             self.assertEqual(response.status_code, 200)
             self.assertIn('Uploaded avatar was set.', response.content)
@@ -148,6 +160,16 @@ class UserAvatarTests(AuthenticatedUserTestCase):
             self.assertTrue(avatar.exists())
             self.assertTrue(avatar.isfile())
 
+            org_avatar_kwargs = {
+                'secret': response_json['options']['crop_org']['secret'],
+                'hash': response_json['avatar_hash'],
+                'user_id': self.user.pk
+            }
+            org_avatar_path = reverse('misago:user_avatar_source',
+                                      kwargs=tmp_avatar_kwargs)
+            response = self.client.get(org_avatar_path)
+            self.assertEqual(response.status_code, 200)
+
             response = self.client.post(self.link, json.dumps({
                     'avatar': 'crop_tmp',
                     'crop': {

+ 1 - 2
misago/users/urls/__init__.py

@@ -76,8 +76,7 @@ urlpatterns += patterns('',
 urlpatterns += patterns('',
     url(r'^user-avatar/', include(patterns('misago.users.views.avatarserver',
         url(r'^(?P<hash>[a-f0-9]+)/(?P<size>\d+)/(?P<user_id>\d+)\.png$', 'serve_user_avatar', name="user_avatar"),
-        url(r'^tmp:(?P<token>[a-zA-Z0-9]+)/(?P<user_id>\d+)\.png$', 'serve_user_avatar_source', kwargs={'suffix': 'tmp'}),
-        url(r'^org:(?P<token>[a-zA-Z0-9]+)/(?P<user_id>\d+)\.png$', 'serve_user_avatar_source', kwargs={'suffix': 'org'}),
+        url(r'^(?P<secret>[a-f0-9]+):(?P<hash>[a-f0-9]+)/(?P<user_id>\d+)\.png$', 'serve_user_avatar_source', name="user_avatar_source"),
         url(r'^(?P<size>\d+)\.png$', 'serve_blank_avatar', name="blank_avatar"),
     )))
 )

+ 6 - 3
misago/users/views/avatarserver.py

@@ -21,7 +21,7 @@ def serve_blank_avatar(request, size):
 
 
 @cache_control(private=True, must_revalidate=False)
-def serve_user_avatar(request, hash, user_id, size):
+def serve_user_avatar(request, user_id, hash, size):
     size = clean_size(size)
 
     if int(user_id) > 0:
@@ -39,14 +39,17 @@ def serve_user_avatar(request, hash, user_id, size):
 
 
 @never_cache
-def serve_user_avatar_source(request, user_id, token, suffix):
+def serve_user_avatar_source(request, user_id, secret, hash):
     fallback_avatar = get_blank_avatar_file(min(settings.MISAGO_AVATARS_SIZES))
     User = get_user_model()
 
     if user_id > 0:
         try:
             user = User.objects.get(id=user_id)
-            if token == store.get_avatar_hash(user, suffix):
+
+            tokens = store.get_user_avatar_tokens(user)
+            suffix = tokens.get(secret)
+            if suffix:
                 avatar_file = get_user_avatar_file(user.pk, suffix)
             else:
                 avatar_file = fallback_avatar