added abuse prevention for share links

This commit is contained in:
vabene1111 2021-06-12 21:15:23 +02:00
parent 9751821f76
commit 08bc87960b
9 changed files with 106 additions and 16 deletions

View File

@ -1,6 +1,8 @@
"""
Source: https://djangosnippets.org/snippets/1703/
"""
from django.conf import settings
from django.core.cache import caches
from django.views.generic.detail import SingleObjectTemplateResponseMixin
from django.views.generic.edit import ModelFormMixin
@ -90,7 +92,18 @@ def share_link_valid(recipe, share):
:return: true if a share link with the given recipe and uuid exists
"""
try:
return True if ShareLink.objects.filter(recipe=recipe, uuid=share).exists() else False
CACHE_KEY = f'recipe_share_{recipe.pk}_{share}'
if c := caches['default'].get(CACHE_KEY, False):
return c
if link := ShareLink.objects.filter(recipe=recipe, uuid=share, abuse_blocked=False).first():
if 0 < settings.SHARING_LIMIT < link.request_count:
return False
link.request_count += 1
link.save()
caches['default'].set(CACHE_KEY, True, timeout=3)
return True
return False
except ValidationError:
return False
@ -121,15 +134,18 @@ class GroupRequiredMixin(object):
def dispatch(self, request, *args, **kwargs):
if not has_group_permission(request.user, self.groups_required):
if not request.user.is_authenticated:
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
messages.add_message(request, messages.ERROR,
_('You are not logged in and therefore cannot view this page!'))
return HttpResponseRedirect(reverse_lazy('account_login') + '?next=' + request.path)
else:
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
messages.add_message(request, messages.ERROR,
_('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
try:
obj = self.get_object()
if obj.get_space() != request.space:
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
messages.add_message(request, messages.ERROR,
_('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
except AttributeError:
pass
@ -141,17 +157,20 @@ class OwnerRequiredMixin(object):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
messages.add_message(request, messages.ERROR, _('You are not logged in and therefore cannot view this page!'))
messages.add_message(request, messages.ERROR,
_('You are not logged in and therefore cannot view this page!'))
return HttpResponseRedirect(reverse_lazy('account_login') + '?next=' + request.path)
else:
if not is_object_owner(request.user, self.get_object()):
messages.add_message(request, messages.ERROR, _('You cannot interact with this object as it is not owned by you!'))
messages.add_message(request, messages.ERROR,
_('You cannot interact with this object as it is not owned by you!'))
return HttpResponseRedirect(reverse('index'))
try:
obj = self.get_object()
if obj.get_space() != request.space:
messages.add_message(request, messages.ERROR, _('You do not have the required permissions to view this page!'))
messages.add_message(request, messages.ERROR,
_('You do not have the required permissions to view this page!'))
return HttpResponseRedirect(reverse_lazy('index'))
except AttributeError:
pass

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-06-12 18:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0131_auto_20210608_1929'),
]
operations = [
migrations.AddField(
model_name='sharelink',
name='request_count',
field=models.IntegerField(default=0),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2021-06-12 18:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0132_sharelink_request_count'),
]
operations = [
migrations.AddField(
model_name='sharelink',
name='abuse_blocked',
field=models.BooleanField(default=False),
),
]

View File

@ -603,6 +603,8 @@ class ShoppingList(ExportModelOperationsMixin('shopping_list'), models.Model, Pe
class ShareLink(ExportModelOperationsMixin('share_link'), models.Model, PermissionModelMixin):
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
uuid = models.UUIDField(default=uuid.uuid4)
request_count = models.IntegerField(default=0)
abuse_blocked = models.BooleanField(default=False)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)

File diff suppressed because one or more lines are too long

View File

@ -64,6 +64,7 @@ urlpatterns = [
path('history/', views.history, name='view_history'),
path('supermarket/', views.supermarket, name='view_supermarket'),
path('files/', views.files, name='view_files'),
path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),
path('test/', views.test, name='view_test'),
path('test2/', views.test2, name='view_test2'),

View File

@ -30,7 +30,7 @@ from cookbook.forms import (CommentForm, Recipe, RecipeBookEntryForm, User,
from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
RecipeBook, RecipeBookEntry, ViewLog, ShoppingList, Space, Keyword, RecipeImport, Unit,
Food, UserFile)
Food, UserFile, ShareLink)
from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall,
ViewLogTable, InviteLinkTable)
from cookbook.views.data import Object
@ -122,7 +122,8 @@ def no_space(request):
request.user.userpreference.save()
request.user.groups.add(Group.objects.filter(name='admin').get())
messages.add_message(request, messages.SUCCESS, _('You have successfully created your own recipe space. Start by adding some recipes or invite other people to join you.'))
messages.add_message(request, messages.SUCCESS,
_('You have successfully created your own recipe space. Start by adding some recipes or invite other people to join you.'))
return HttpResponseRedirect(reverse('index'))
if join_form.is_valid():
@ -239,10 +240,12 @@ def supermarket(request):
@group_required('user')
def files(request):
try:
current_file_size_mb = UserFile.objects.filter(space=request.space).aggregate(Sum('file_size_kb'))['file_size_kb__sum'] / 1000
current_file_size_mb = UserFile.objects.filter(space=request.space).aggregate(Sum('file_size_kb'))[
'file_size_kb__sum'] / 1000
except TypeError:
current_file_size_mb = 0
return render(request, 'files.html', {'current_file_size_mb': current_file_size_mb, 'max_file_size_mb': request.space.max_file_storage_mb})
return render(request, 'files.html',
{'current_file_size_mb': current_file_size_mb, 'max_file_size_mb': request.space.max_file_storage_mb})
@group_required('user')
@ -264,7 +267,9 @@ def meal_plan_entry(request, pk):
@group_required('user')
def latest_shopping_list(request):
sl = ShoppingList.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).filter(finished=False, pace=request.space).order_by('-created_at').first()
sl = ShoppingList.objects.filter(Q(created_by=request.user) | Q(shared=request.user)).filter(finished=False,
pace=request.space).order_by(
'-created_at').first()
if sl:
return HttpResponseRedirect(reverse('view_shopping', kwargs={'pk': sl.pk}) + '?edit=true')
@ -438,7 +443,8 @@ def invite_link(request, token):
if link := InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, uuid=token).first():
if request.user.is_authenticated:
if request.user.userpreference.space:
messages.add_message(request, messages.WARNING, _('You are already member of a space and therefore cannot join this one.'))
messages.add_message(request, messages.WARNING,
_('You are already member of a space and therefore cannot join this one.'))
return HttpResponseRedirect(reverse('index'))
link.used_by = request.user
@ -481,7 +487,8 @@ def space(request):
counts.recipes_no_keyword = Recipe.objects.filter(keywords=None, space=request.space).count()
invite_links = InviteLinkTable(InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, space=request.space).all())
invite_links = InviteLinkTable(
InviteLink.objects.filter(valid_until__gte=datetime.today(), used_by=None, space=request.space).all())
RequestConfig(request, paginate={'per_page': 25}).configure(invite_links)
return render(request, 'space.html', {'space_users': space_users, 'counts': counts, 'invite_links': invite_links})
@ -515,6 +522,19 @@ def space_change_member(request, user_id, space_id, group):
return HttpResponseRedirect(reverse('view_space'))
def report_share_abuse(request, token):
if not settings.SHARING_ABUSE:
messages.add_message(request, messages.WARNING,
_('Reporting share links is not enabled for this instance. Please notify the page administrator to report problems.'))
else:
if link := ShareLink.objects.filter(uuid=token).first():
link.abuse_blocked = True
link.save()
messages.add_message(request, messages.WARNING,
_('Recipe sharing link has been disabled! For additional information please contact the page administrator.'))
return HttpResponseRedirect(reverse('index'))
def markdown_info(request):
return render(request, 'markdown_info.html', {})

View File

@ -67,6 +67,9 @@ DJANGO_TABLES2_PAGE_RANGE = 8
HCAPTCHA_SITEKEY = os.getenv('HCAPTCHA_SITEKEY', '')
HCAPTCHA_SECRET = os.getenv('HCAPTCHA_SECRET', '')
SHARING_ABUSE = bool(int(os.getenv('SHARING_ABUSE', False)))
SHARING_LIMIT = int(os.getenv('SHARING_LIMIT', 0))
ACCOUNT_SIGNUP_FORM_CLASS = 'cookbook.forms.AllAuthSignupForm'
TERMS_URL = os.getenv('TERMS_URL', '')

View File

@ -134,6 +134,14 @@
</div>
<add-recipe-to-book :recipe="recipe"></add-recipe-to-book>
<div class="row text-center" style="margin-top: 3vh; margin-bottom: 3vh" v-if="share_uid !== ''">
<div class="col col-md-12">
<a :href="resolveDjangoUrl('view_report_share_abuse', share_uid)" >{{$t('Report Abuse')}}</a>
</div>
</div>
</div>
</template>
@ -191,7 +199,8 @@ export default {
recipe: undefined,
ingredient_count: 0,
servings: 1,
start_time: ""
start_time: "",
share_uid: window.SHARE_UID
}
},
mounted() {