Draft: Two factor auth #118

Open
sonny wants to merge 9 commits from two-factor-auth into development
7 changed files with 40 additions and 38 deletions
Showing only changes of commit 4e52a4e867 - Show all commits

View file

@ -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()

View file

@ -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

View file

@ -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",),

View file

@ -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/<int:pk>/", login_required(FeedUpdateView.as_view()), name="feed-update"
),
path("feeds/create/", login_required(FeedCreateView.as_view()), name="feed-create"),
path("feeds/<int:pk>/", 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/<int:pk>/",
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/<int:pk>/",
login_required(TwitterTimelineUpdateView.as_view()),
otp_required(TwitterTimelineUpdateView.as_view()),
name="twitter-timeline-update",
),
]

View file

@ -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):

View file

@ -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/<int:pk>/",
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",
),
]

View file

@ -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"),