-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Backend Security Audit fixes #352
Changes from 6 commits
b585295
3204c7e
ca03c46
cc77cd0
73b18a1
810ea86
6b17024
69aabe8
c7d7b0b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,4 +16,4 @@ class Meta: | |
abstract = True | ||
|
||
def __str__(self): | ||
return "ah yes" | ||
return "__str__ not defined for this model" | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,7 +24,7 @@ class CustomUserAdmin(UserAdmin): | |
) | ||
}, | ||
), | ||
("Admin Options", {"classes": ("collapse",), "fields": ("is_staff", "groups")}), | ||
("Admin Options", {"classes": ("collapse",), "fields": ("is_active", "is_staff", "is_superuser", "groups")}), | ||
) | ||
add_fieldsets = ( | ||
( | ||
|
@@ -35,7 +35,15 @@ class CustomUserAdmin(UserAdmin): | |
}, | ||
), | ||
) | ||
list_display = ("email", "first_name", "last_name", "is_active", "is_staff", "is_superuser", "permissions") | ||
list_display = ( | ||
"email", | ||
"permissions", | ||
"is_active", | ||
"is_staff", | ||
"is_superuser", | ||
"first_name", | ||
"last_name", | ||
) | ||
list_display_links = ( | ||
"is_active", | ||
"email", | ||
|
@@ -57,7 +65,7 @@ class CustomUserAdmin(UserAdmin): | |
ordering = [] | ||
|
||
def permissions(self, obj): | ||
return ", ".join([g.name for g in obj.groups.all()]) | ||
return [g.name for g in obj.groups.all()] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @oudeismetis - I trust that you checked that this works? This is changing the return type from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. correct. I pulled this from a project where I made the change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great! This is good to go then. |
||
|
||
class Media(AutocompleteAdminMedia): | ||
pass | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
from rest_framework import permissions | ||
|
||
|
||
class CreateOnlyPermissions(permissions.BasePermission): | ||
def has_permission(self, request, view): | ||
if view.action == "create": | ||
return True | ||
return False | ||
class HasUserPermissions(permissions.BasePermission): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Q, is there a situation where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should not happen ever There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Notice that the viewset where this is used also handles user creation requests. There are automated tests that cover that case. I do think I've had to handle some sort of none case in the past, but I haven't seen that in a long while. But should that bug arise it would be a small/quick/obvious fix There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the Authentication Middleware always adds There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That tracks. Otherwise There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah nvm this is used on the object level! |
||
"""Admins should be able to perform any action, regular users should be able to edit and delete self.""" | ||
|
||
def has_object_permission(self, request, view, obj): | ||
return request.user.is_authenticated and (request.user.is_staff or obj == request.user) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ class Meta: | |
"last_name", | ||
"full_name", | ||
) | ||
read_only_fields = ["email"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Default behavior shouldn't allow changing of a users email just by having a valid token. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @paribaker I know you had thoughts about this one |
||
|
||
|
||
class UserLoginSerializer(serializers.ModelSerializer): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,11 +18,11 @@ | |
from rest_framework.exceptions import ValidationError | ||
from rest_framework.response import Response | ||
|
||
from {{ cookiecutter.project_slug }}.core.forms import PreviewTemplateForm | ||
from {{ cookiecutter.project_slug }}.utils.emails import send_html_email | ||
|
||
from .forms import PreviewTemplateForm | ||
from .models import User | ||
from .permissions import CreateOnlyPermissions | ||
from .permissions import HasUserPermissions | ||
from .serializers import UserLoginSerializer, UserRegistrationSerializer, UserSerializer | ||
|
||
logger = logging.getLogger(__name__) | ||
|
@@ -50,45 +50,45 @@ def post(self, request, *args, **kwargs): | |
return Response(response_data) | ||
|
||
|
||
class UserViewSet( | ||
viewsets.GenericViewSet, | ||
mixins.RetrieveModelMixin, | ||
mixins.ListModelMixin, | ||
mixins.UpdateModelMixin, | ||
mixins.DestroyModelMixin, | ||
): | ||
queryset = User.objects.all() | ||
class UserViewSet(viewsets.GenericViewSet, mixins.RetrieveModelMixin): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We implement some of these with overrides below. |
||
serializer_class = UserSerializer | ||
|
||
# No auth required to create user | ||
# Auth required for all other actions | ||
permission_classes = (permissions.IsAuthenticated | CreateOnlyPermissions,) | ||
permission_classes = (HasUserPermissions,) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Create still works fine. There are automated tests now for all of this so should this break on a project, a dev would have to also remove the test to get around it. |
||
|
||
@transaction.atomic | ||
def create(self, request, *args, **kwargs): | ||
def get_queryset(self): | ||
""" | ||
Endpoint to create/register a new user. | ||
Users should only find themselves by default | ||
""" | ||
user = self.request.user | ||
return User.objects.filter(pk=user.pk) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @whusterj I removed the querset and replaced it with this call. It might not matter since we don't have list as an option here anymore. But probably a good default to have for projects that try adding a quick mixin here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. aaaand it's broken the tests. Will investigate in the morning. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had to add the queryset back. Left a comment as to why |
||
|
||
@transaction.atomic | ||
def create(self, request, *args, **kwargs): | ||
serializer = UserRegistrationSerializer(data=request.data) | ||
serializer.is_valid(raise_exception=True) | ||
serializer.save() # This calls .create() on serializer | ||
user = serializer.instance | ||
|
||
user = serializer.save() | ||
# Log-in user and re-serialize response | ||
response_data = UserLoginSerializer.login(user, request) | ||
return Response(response_data, status=status.HTTP_201_CREATED) | ||
|
||
def update(self, request, *args, **kwargs): | ||
""" | ||
Endpoint to create/register a new user. | ||
""" | ||
serializer = UserSerializer(data=request.data, instance=self.get_object(), partial=True) | ||
|
||
serializer.is_valid(raise_exception=True) | ||
serializer.save() | ||
user = serializer.data | ||
return Response(serializer.data, status=status.HTTP_200_OK) | ||
|
||
return Response(user, status=status.HTTP_200_OK) | ||
def destroy(self, request, *args, **kwargs): | ||
""" | ||
When deleting a user's account, just disable their account first | ||
The user may have a regret and try to get their account back | ||
A background job should then properly delete the data after X days | ||
""" | ||
user = self.get_object() | ||
user.is_active = False | ||
user.save() | ||
return Response(status=status.HTTP_204_NO_CONTENT) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment above should be clear. |
||
|
||
|
||
@api_view(["post"]) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,9 @@ | |
if CURRENT_DOMAIN not in ALLOWED_HOSTS: | ||
ALLOWED_HOSTS.append(CURRENT_DOMAIN) | ||
|
||
# Used by the corsheaders app/middleware (django-cors-headers) to allow multiple domains to access the backend | ||
CORS_ALLOWED_ORIGINS = [f"https://{host}" for host in ALLOWED_HOSTS] | ||
|
||
# Application definition | ||
|
||
INSTALLED_APPS = [ | ||
|
@@ -306,7 +309,6 @@ | |
if not IN_DEV: | ||
SECURE_SSL_REDIRECT = True | ||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") | ||
MIDDLEWARE += ["django.middleware.security.SecurityMiddleware"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. duplicate from another spot in this file. |
||
|
||
# | ||
# Custom logging configuration | ||
|
@@ -390,11 +392,6 @@ def filter(self, record): | |
# Popular testing framework that allows logging to stdout while running unit tests | ||
TEST_RUNNER = "django_nose.NoseTestSuiteRunner" | ||
|
||
CORS_ALLOWED_ORIGINS = ["https://{{ cookiecutter.project_slug|replace('_', '-') }}-staging.herokuapp.com", "https://{{ cookiecutter.project_slug|replace('_', '-') }}.herokuapp.com"] | ||
{% if cookiecutter.client_app.lower() != 'none' -%} | ||
CORS_ALLOWED_ORIGINS.append("http://localhost:8080") | ||
{% endif -%} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See further up. |
||
SWAGGER_SETTINGS = { | ||
"LOGIN_URL": "/login", | ||
"USE_SESSION_AUTH": False, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,9 @@ | ||
from django.contrib import admin | ||
from django.urls import include, path | ||
|
||
admin.site.site_header = "{{ cookiecutter.project_name }} Admin" | ||
admin.site.site_title = "{{ cookiecutter.project_name }}" | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a nice little addition so that managing multiple projects makes it more obvious which admin you are in. Also, non-dev project stakeholders don't know (or need to care) what the "Django Admin" is. It's just the admin for the app. |
||
urlpatterns = [ | ||
path(r"staff/", admin.site.urls), | ||
path(r"", include("{{ cookiecutter.project_slug }}.common.favicon_urls")), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gentle nudge for devs to set this for their new models
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you