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): class IsOwner(BasePermission):
@ -21,3 +21,9 @@ class IsPostOwner(BasePermission):
return bool(is_category_user and is_rule_user) return bool(is_category_user and is_rule_user)
return 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.urls import include, path
from django_otp.decorators import otp_required
from two_factor.views import ( from two_factor.views import (
BackupTokensView, BackupTokensView,
DisableView, DisableView,
@ -43,42 +43,43 @@ settings_patterns = [
# Integrations # Integrations
path( path(
"integrations/reddit/callback/", "integrations/reddit/callback/",
login_required(RedditTemplateView.as_view()), otp_required(RedditTemplateView.as_view()),
name="reddit-template", name="reddit-template",
), ),
path( path(
"integrations/reddit/refresh/", "integrations/reddit/refresh/",
login_required(RedditTokenRedirectView.as_view()), otp_required(RedditTokenRedirectView.as_view()),
name="reddit-refresh", name="reddit-refresh",
), ),
path( path(
"integrations/reddit/revoke/", "integrations/reddit/revoke/",
login_required(RedditRevokeRedirectView.as_view()), otp_required(RedditRevokeRedirectView.as_view()),
name="reddit-revoke", name="reddit-revoke",
), ),
path( path(
"integrations/twitter/auth/", "integrations/twitter/auth/",
login_required(TwitterAuthRedirectView.as_view()), otp_required(TwitterAuthRedirectView.as_view()),
name="twitter-auth", name="twitter-auth",
), ),
path( path(
"integrations/twitter/callback/", "integrations/twitter/callback/",
login_required(TwitterTemplateView.as_view()), otp_required(TwitterTemplateView.as_view()),
name="twitter-template", name="twitter-template",
), ),
path( path(
"integrations/twitter/revoke/", "integrations/twitter/revoke/",
login_required(TwitterRevokeRedirectView.as_view()), otp_required(TwitterRevokeRedirectView.as_view()),
name="twitter-revoke", name="twitter-revoke",
), ),
path( path(
"integrations/", login_required(IntegrationsView.as_view()), name="integrations" "integrations/", otp_required(IntegrationsView.as_view()), name="integrations"
), ),
# Misc # Misc
path("favicon/", login_required(FaviconRedirectView.as_view()), name="favicon"), path("favicon/", otp_required(FaviconRedirectView.as_view()), name="favicon"),
path("", login_required(SettingsView.as_view()), name="home"), path("", otp_required(SettingsView.as_view()), name="home"),
] ]
# permissions are handled through the views itself
two_factor = [ two_factor = [
path("accounts/setup/", SetupView.as_view(), name="setup"), path("accounts/setup/", SetupView.as_view(), name="setup"),
path("accounts/qrcode/", QRGeneratorView.as_view(), name="qr"), path("accounts/qrcode/", QRGeneratorView.as_view(), name="qr"),
@ -140,7 +141,7 @@ urlpatterns = [
), ),
path( path(
"password-change/", "password-change/",
login_required(PasswordChangeView.as_view()), otp_required(PasswordChangeView.as_view()),
name="password-change", name="password-change",
), ),
# Settings # Settings

View file

@ -243,7 +243,7 @@ REST_FRAMEWORK = {
"rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.SessionAuthentication",
), ),
"DEFAULT_PERMISSION_CLASSES": ( "DEFAULT_PERMISSION_CLASSES": (
"rest_framework.permissions.IsAuthenticated", "newsreader.accounts.permissions.TwoFactorAuthenticated",
"newsreader.accounts.permissions.IsOwner", "newsreader.accounts.permissions.IsOwner",
), ),
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), "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.urls import path
from django_otp.decorators import otp_required
from newsreader.news.collection.endpoints import ( from newsreader.news.collection.endpoints import (
DetailRuleView, DetailRuleView,
NestedRuleView, NestedRuleView,
@ -29,48 +30,46 @@ endpoints = [
urlpatterns = [ urlpatterns = [
# Feeds # Feeds
path( path("feeds/<int:pk>/", otp_required(FeedUpdateView.as_view()), name="feed-update"),
"feeds/<int:pk>/", login_required(FeedUpdateView.as_view()), name="feed-update" path("feeds/create/", otp_required(FeedCreateView.as_view()), name="feed-create"),
),
path("feeds/create/", login_required(FeedCreateView.as_view()), name="feed-create"),
# Generic rules # Generic rules
path("rules/", login_required(CollectionRuleListView.as_view()), name="rules"), path("rules/", otp_required(CollectionRuleListView.as_view()), name="rules"),
path( path(
"rules/delete/", "rules/delete/",
login_required(CollectionRuleBulkDeleteView.as_view()), otp_required(CollectionRuleBulkDeleteView.as_view()),
name="rules-delete", name="rules-delete",
), ),
path( path(
"rules/enable/", "rules/enable/",
login_required(CollectionRuleBulkEnableView.as_view()), otp_required(CollectionRuleBulkEnableView.as_view()),
name="rules-enable", name="rules-enable",
), ),
path( path(
"rules/disable/", "rules/disable/",
login_required(CollectionRuleBulkDisableView.as_view()), otp_required(CollectionRuleBulkDisableView.as_view()),
name="rules-disable", name="rules-disable",
), ),
path("rules/import/", login_required(OPMLImportView.as_view()), name="import"), path("rules/import/", otp_required(OPMLImportView.as_view()), name="import"),
# Reddit # Reddit
path( path(
"subreddits/create/", "subreddits/create/",
login_required(SubRedditCreateView.as_view()), otp_required(SubRedditCreateView.as_view()),
name="subreddit-create", name="subreddit-create",
), ),
path( path(
"subreddits/<int:pk>/", "subreddits/<int:pk>/",
login_required(SubRedditUpdateView.as_view()), otp_required(SubRedditUpdateView.as_view()),
name="subreddit-update", name="subreddit-update",
), ),
# Twitter # Twitter
path( path(
"twitter/timelines/create/", "twitter/timelines/create/",
login_required(TwitterTimelineCreateView.as_view()), otp_required(TwitterTimelineCreateView.as_view()),
name="twitter-timeline-create", name="twitter-timeline-create",
), ),
path( path(
"twitter/timelines/<int:pk>/", "twitter/timelines/<int:pk>/",
login_required(TwitterTimelineUpdateView.as_view()), otp_required(TwitterTimelineUpdateView.as_view()),
name="twitter-timeline-update", name="twitter-timeline-update",
), ),
] ]

View file

@ -7,10 +7,8 @@ from rest_framework.generics import (
RetrieveUpdateDestroyAPIView, RetrieveUpdateDestroyAPIView,
get_object_or_404, get_object_or_404,
) )
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from newsreader.accounts.permissions import IsPostOwner
from newsreader.core.pagination import CursorPagination from newsreader.core.pagination import CursorPagination
from newsreader.news.collection.serializers import RuleSerializer from newsreader.news.collection.serializers import RuleSerializer
from newsreader.news.core.filters import ReadFilter, SavedFilter from newsreader.news.core.filters import ReadFilter, SavedFilter
@ -21,7 +19,6 @@ from newsreader.news.core.serializers import CategorySerializer, PostSerializer
class ListPostView(ListAPIView): class ListPostView(ListAPIView):
queryset = Post.objects.all() queryset = Post.objects.all()
serializer_class = PostSerializer serializer_class = PostSerializer
permission_classes = (IsAuthenticated, IsPostOwner)
pagination_class = CursorPagination pagination_class = CursorPagination
filter_backends = [ReadFilter, SavedFilter] filter_backends = [ReadFilter, SavedFilter]
@ -29,7 +26,6 @@ class ListPostView(ListAPIView):
class DetailPostView(RetrieveUpdateAPIView): class DetailPostView(RetrieveUpdateAPIView):
queryset = Post.objects.all() queryset = Post.objects.all()
serializer_class = PostSerializer serializer_class = PostSerializer
permission_classes = (IsAuthenticated, IsPostOwner)
class ListCategoryView(ListAPIView): class ListCategoryView(ListAPIView):

View file

@ -1,6 +1,7 @@
from django.contrib.auth.decorators import login_required
from django.urls import path from django.urls import path
from django_otp.decorators import otp_required
from newsreader.news.core.endpoints import ( from newsreader.news.core.endpoints import (
CategoryReadView, CategoryReadView,
DetailCategoryView, DetailCategoryView,
@ -14,20 +15,19 @@ from newsreader.news.core.views import (
CategoryCreateView, CategoryCreateView,
CategoryListView, CategoryListView,
CategoryUpdateView, CategoryUpdateView,
NewsView,
) )
urlpatterns = [ urlpatterns = [
path("categories/", login_required(CategoryListView.as_view()), name="categories"), path("categories/", otp_required(CategoryListView.as_view()), name="categories"),
path( path(
"categories/<int:pk>/", "categories/<int:pk>/",
login_required(CategoryUpdateView.as_view()), otp_required(CategoryUpdateView.as_view()),
name="category-update", name="category-update",
), ),
path( path(
"categories/create/", "categories/create/",
login_required(CategoryCreateView.as_view()), otp_required(CategoryCreateView.as_view()),
name="category-create", name="category-create",
), ),
] ]

View file

@ -1,8 +1,8 @@
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.decorators import login_required
from django.urls import include, path from django.urls import include, path
from django_otp.decorators import otp_required
from drf_yasg import openapi from drf_yasg import openapi
from drf_yasg.views import get_schema_view from drf_yasg.views import get_schema_view
from two_factor.admin import AdminSiteOTPRequired from two_factor.admin import AdminSiteOTPRequired
@ -21,7 +21,7 @@ schema_view = get_schema_view(schema_info, patterns=api_patterns)
admin.site.__class__ = AdminSiteOTPRequired admin.site.__class__ = AdminSiteOTPRequired
urlpatterns = [ urlpatterns = [
path("", login_required(NewsView.as_view()), name="index"), path("", otp_required(NewsView.as_view()), name="index"),
path("", include((news_patterns, "news"))), path("", include((news_patterns, "news"))),
path("", include((api_patterns, "api"))), path("", include((api_patterns, "api"))),
path("accounts/", include((login_urls, "accounts")), name="accounts"), path("accounts/", include((login_urls, "accounts")), name="accounts"),