diff --git a/src/newsreader/accounts/permissions.py b/src/newsreader/accounts/permissions.py index 2c6cf25..8e19735 100644 --- a/src/newsreader/accounts/permissions.py +++ b/src/newsreader/accounts/permissions.py @@ -1,4 +1,4 @@ -from rest_framework.permissions import BasePermission +from rest_framework.permissions import BasePermission, IsAuthenticated class IsOwner(BasePermission): @@ -21,3 +21,9 @@ class IsPostOwner(BasePermission): return bool(is_category_user and is_rule_user) return is_rule_user + + +class TwoFactorAuthenticated(IsAuthenticated): + def has_permission(self, request, view): + is_authenticated = super().has_permission(request, view) + return is_authenticated and request.user.is_verified() diff --git a/src/newsreader/accounts/urls.py b/src/newsreader/accounts/urls.py index 5784b7f..9962509 100644 --- a/src/newsreader/accounts/urls.py +++ b/src/newsreader/accounts/urls.py @@ -1,6 +1,6 @@ -from django.contrib.auth.decorators import login_required from django.urls import include, path +from django_otp.decorators import otp_required from two_factor.views import ( BackupTokensView, DisableView, @@ -43,42 +43,43 @@ settings_patterns = [ # Integrations path( "integrations/reddit/callback/", - login_required(RedditTemplateView.as_view()), + otp_required(RedditTemplateView.as_view()), name="reddit-template", ), path( "integrations/reddit/refresh/", - login_required(RedditTokenRedirectView.as_view()), + otp_required(RedditTokenRedirectView.as_view()), name="reddit-refresh", ), path( "integrations/reddit/revoke/", - login_required(RedditRevokeRedirectView.as_view()), + otp_required(RedditRevokeRedirectView.as_view()), name="reddit-revoke", ), path( "integrations/twitter/auth/", - login_required(TwitterAuthRedirectView.as_view()), + otp_required(TwitterAuthRedirectView.as_view()), name="twitter-auth", ), path( "integrations/twitter/callback/", - login_required(TwitterTemplateView.as_view()), + otp_required(TwitterTemplateView.as_view()), name="twitter-template", ), path( "integrations/twitter/revoke/", - login_required(TwitterRevokeRedirectView.as_view()), + otp_required(TwitterRevokeRedirectView.as_view()), name="twitter-revoke", ), path( - "integrations/", login_required(IntegrationsView.as_view()), name="integrations" + "integrations/", otp_required(IntegrationsView.as_view()), name="integrations" ), # Misc - path("favicon/", login_required(FaviconRedirectView.as_view()), name="favicon"), - path("", login_required(SettingsView.as_view()), name="home"), + path("favicon/", otp_required(FaviconRedirectView.as_view()), name="favicon"), + path("", otp_required(SettingsView.as_view()), name="home"), ] +# permissions are handled through the views itself two_factor = [ path("accounts/setup/", SetupView.as_view(), name="setup"), path("accounts/qrcode/", QRGeneratorView.as_view(), name="qr"), @@ -140,7 +141,7 @@ urlpatterns = [ ), path( "password-change/", - login_required(PasswordChangeView.as_view()), + otp_required(PasswordChangeView.as_view()), name="password-change", ), # Settings diff --git a/src/newsreader/conf/base.py b/src/newsreader/conf/base.py index 99f5b44..5d7db19 100644 --- a/src/newsreader/conf/base.py +++ b/src/newsreader/conf/base.py @@ -243,7 +243,7 @@ REST_FRAMEWORK = { "rest_framework.authentication.SessionAuthentication", ), "DEFAULT_PERMISSION_CLASSES": ( - "rest_framework.permissions.IsAuthenticated", + "newsreader.accounts.permissions.TwoFactorAuthenticated", "newsreader.accounts.permissions.IsOwner", ), "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), diff --git a/src/newsreader/news/collection/urls.py b/src/newsreader/news/collection/urls.py index e5276cb..8ffd1c2 100644 --- a/src/newsreader/news/collection/urls.py +++ b/src/newsreader/news/collection/urls.py @@ -1,6 +1,7 @@ -from django.contrib.auth.decorators import login_required from django.urls import path +from django_otp.decorators import otp_required + from newsreader.news.collection.endpoints import ( DetailRuleView, NestedRuleView, @@ -29,48 +30,46 @@ endpoints = [ urlpatterns = [ # Feeds - path( - "feeds//", login_required(FeedUpdateView.as_view()), name="feed-update" - ), - path("feeds/create/", login_required(FeedCreateView.as_view()), name="feed-create"), + path("feeds//", otp_required(FeedUpdateView.as_view()), name="feed-update"), + path("feeds/create/", otp_required(FeedCreateView.as_view()), name="feed-create"), # Generic rules - path("rules/", login_required(CollectionRuleListView.as_view()), name="rules"), + path("rules/", otp_required(CollectionRuleListView.as_view()), name="rules"), path( "rules/delete/", - login_required(CollectionRuleBulkDeleteView.as_view()), + otp_required(CollectionRuleBulkDeleteView.as_view()), name="rules-delete", ), path( "rules/enable/", - login_required(CollectionRuleBulkEnableView.as_view()), + otp_required(CollectionRuleBulkEnableView.as_view()), name="rules-enable", ), path( "rules/disable/", - login_required(CollectionRuleBulkDisableView.as_view()), + otp_required(CollectionRuleBulkDisableView.as_view()), name="rules-disable", ), - path("rules/import/", login_required(OPMLImportView.as_view()), name="import"), + path("rules/import/", otp_required(OPMLImportView.as_view()), name="import"), # Reddit path( "subreddits/create/", - login_required(SubRedditCreateView.as_view()), + otp_required(SubRedditCreateView.as_view()), name="subreddit-create", ), path( "subreddits//", - login_required(SubRedditUpdateView.as_view()), + otp_required(SubRedditUpdateView.as_view()), name="subreddit-update", ), # Twitter path( "twitter/timelines/create/", - login_required(TwitterTimelineCreateView.as_view()), + otp_required(TwitterTimelineCreateView.as_view()), name="twitter-timeline-create", ), path( "twitter/timelines//", - login_required(TwitterTimelineUpdateView.as_view()), + otp_required(TwitterTimelineUpdateView.as_view()), name="twitter-timeline-update", ), ] diff --git a/src/newsreader/news/core/endpoints.py b/src/newsreader/news/core/endpoints.py index b224024..3cace4a 100644 --- a/src/newsreader/news/core/endpoints.py +++ b/src/newsreader/news/core/endpoints.py @@ -7,10 +7,8 @@ from rest_framework.generics import ( RetrieveUpdateDestroyAPIView, get_object_or_404, ) -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from newsreader.accounts.permissions import IsPostOwner from newsreader.core.pagination import CursorPagination from newsreader.news.collection.serializers import RuleSerializer from newsreader.news.core.filters import ReadFilter, SavedFilter @@ -21,7 +19,6 @@ from newsreader.news.core.serializers import CategorySerializer, PostSerializer class ListPostView(ListAPIView): queryset = Post.objects.all() serializer_class = PostSerializer - permission_classes = (IsAuthenticated, IsPostOwner) pagination_class = CursorPagination filter_backends = [ReadFilter, SavedFilter] @@ -29,7 +26,6 @@ class ListPostView(ListAPIView): class DetailPostView(RetrieveUpdateAPIView): queryset = Post.objects.all() serializer_class = PostSerializer - permission_classes = (IsAuthenticated, IsPostOwner) class ListCategoryView(ListAPIView): diff --git a/src/newsreader/news/core/urls.py b/src/newsreader/news/core/urls.py index 8096cf8..2d6c78a 100644 --- a/src/newsreader/news/core/urls.py +++ b/src/newsreader/news/core/urls.py @@ -1,6 +1,7 @@ -from django.contrib.auth.decorators import login_required from django.urls import path +from django_otp.decorators import otp_required + from newsreader.news.core.endpoints import ( CategoryReadView, DetailCategoryView, @@ -14,20 +15,19 @@ from newsreader.news.core.views import ( CategoryCreateView, CategoryListView, CategoryUpdateView, - NewsView, ) urlpatterns = [ - path("categories/", login_required(CategoryListView.as_view()), name="categories"), + path("categories/", otp_required(CategoryListView.as_view()), name="categories"), path( "categories//", - login_required(CategoryUpdateView.as_view()), + otp_required(CategoryUpdateView.as_view()), name="category-update", ), path( "categories/create/", - login_required(CategoryCreateView.as_view()), + otp_required(CategoryCreateView.as_view()), name="category-create", ), ] diff --git a/src/newsreader/urls.py b/src/newsreader/urls.py index e416d5d..6069607 100644 --- a/src/newsreader/urls.py +++ b/src/newsreader/urls.py @@ -1,8 +1,8 @@ from django.conf import settings from django.contrib import admin -from django.contrib.auth.decorators import login_required from django.urls import include, path +from django_otp.decorators import otp_required from drf_yasg import openapi from drf_yasg.views import get_schema_view from two_factor.admin import AdminSiteOTPRequired @@ -21,7 +21,7 @@ schema_view = get_schema_view(schema_info, patterns=api_patterns) admin.site.__class__ = AdminSiteOTPRequired urlpatterns = [ - path("", login_required(NewsView.as_view()), name="index"), + path("", otp_required(NewsView.as_view()), name="index"), path("", include((news_patterns, "news"))), path("", include((api_patterns, "api"))), path("accounts/", include((login_urls, "accounts")), name="accounts"),