Custom Authentication for non-user connection with Django Rest Framework - django

I have enabled user authentication with DRF using TokenAuthentication
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication'
),
'DEFAULT_MODEL_SERIALIZER_CLASS':
'rest_framework.serializers.ModelSerializer',
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
),
#'EXCEPTION_HANDLER': 'apps.core.exceptions.custom_exception_handler'
}
I have the following model:
class Device(CreationModificationMixin):
"""
Contains devices (WW controllers). A device may be associated with the Owner
"""
_STATUSES = (
('A', 'Active'), # when everything is okay
('I', 'Inactive'), # when we got nothing from SPA controllers for X minutes
('F', 'Failure'), # when controller says it has issues
)
_TYPES = (
('S', 'Spa'),
('P', 'Pool'),
)
udid = models.CharField(max_length=255, verbose_name="Unique ID / MAC Address", help_text="MAC Address of WiFi controller", unique=True, null=False, blank=False, db_index=True)
type = models.CharField(max_length=1, choices=_TYPES, null=False, blank=False)
title = models.CharField(max_length=255, null=False, blank=False, db_index=True)
status = models.CharField(max_length=1, default='A', choices=_STATUSES)
pinged = models.DateTimeField(null=True)
owner = models.ForeignKey(Owner, verbose_name="Owner", null=True, blank=True, db_index=True)
def __str__(self):
return self.udid
This represents hardware device that will be sending discrete requests to API endpoints, therefore I need to authenticate each request and ideally with token based identification, like
POST /api/devices/login
{
udid: '...mac address...',
hash: '...sha256...hash string',
time: '2015-01-01 12:24:30'
}
hash will be calculated on device side as sha256(salt + udid + current_time)
the same hash will be calculated on DRF side inside /login to compare and generate token that will be saved in REDIS and returned back with response.
All future requests will be passing this token as a header, which will be checked in custom Permission class.
my questions:
I'd like to set a custom property on request class, like
request.device, request.device.is_authenticated()
Where should I put this functionality?
Do you see something wrong in my approach? Maybe a recommendation for improvements?

As #daniel-van-flymen pointed out, it's probably not a good idea to return a device instead of a user. So what I did was create a DeviceUser class that extends django.contrib.auth.models.AnonymousUserand return that in my custom authentication (devices are essentially anonymous users, after all).
from myapp.models import Device
from rest_framework import authentication
from django.contrib.auth.models import AnonymousUser
from rest_framework.exceptions import AuthenticationFailed
class DeviceUser(AnonymousUser):
def __init__(self, device):
self.device = device
#property
def is_authenticated(self):
return True
class DeviceAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
udid = request.META.get("HTTP_X_UDID", None)
if not udid:
return None
try:
device = Device.objects.get(udid=udid)
except Device.DoesNotExist:
raise AuthenticationFailed("Invalid UDID")
if not device.active:
raise AuthenticationFailed("Device is inactive or deleted")
request.device = device
return (DeviceUser(device), None)
This code lives in myapp.authentication, you can then add the following to your settings:
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"myapp.authentication.DeviceAuthentication",
)
}
A couple of notes from your original spec: I've modified the request in the authenticator to include the device, so you can do request.device.is_authenticated; however, the user will be a DeviceUser so you could also do request.user.device.is_authenticated (so long as you do the appropriate checks for the device attribute).
Your original spec also asked to implement TokenAuthentication, and it is possible to subclass this authentication class to use it more directly; for simplicity, I'm just having the device include the X-UDID header in their request.
Also note that as with the token authentication mechanism, you must use this method with HTTPS, otherwise the UDID will be sent in plain text, allowing someone to impersonate a device.

You can subclass DRF's BaseAuthentication class and override the .authenticate(self, request) method.
On successful auth this function should return (device, None). This will set device object in request.user property.
You can implement is_authenticated() in your Device model class.
class APICustomAuthentication(BaseAuthentication):
---
def authenticate(self, request):
----
return (device, None) # on successful authentication
Add APICustomAuthentication to 'DEFAULT_AUTHENTICATION_CLASSES'
in settings.
More details are available here

Related

django factory using a factory gives "Database access not allowed"

I want to create a user_profile from a factory called UserProfileFactory which uses a User object from UserFactory.
the error is: RuntimeError: Database access not allowed, use the "django_db" mark, or the "db" or "transactional_db" fixtures to enable it.
here are the relivant classes.
from django.contrib.auth import get_user_model
from factory import Faker, post_generation
from factory.django import DjangoModelFactory
class UserFactory(DjangoModelFactory):
username = Faker("user_name")
email = Faker("email")
name = Faker("name")
#post_generation
def password(self, create: bool, extracted: Sequence[Any], **kwargs):
password = (
extracted
if extracted
else Faker(
"password",
length=42,
special_chars=True,
digits=True,
upper_case=True,
lower_case=True,
).evaluate(None, None, extra={"locale": None})
)
self.set_password(password)
class Meta:
model = get_user_model()
django_get_or_create = ["username"]
class UserProfileFactory(DjangoModelFactory):
user = UserFactory.create() #### the problem line ###
country = Faker("country") # which laws apply
birth_date = Faker("date_of_birth") # in the US you can't collect data from <13yo's
class Meta:
model = UserProfile
and in the tests/test.py
class TestUserProfileDetailView():
def test_create_userprofile(self):
"""creates an APIRequest and uses an instance of UserProfile to test a view user_detail_view"""
factory = APIRequestFactory()
request = factory.get('/api/userprofile/')
request.user = UserProfileFactory.create() # problem starts here #
response = user_detail_view(request)
self.assertEqual(response.status_code, 200)
(I know this is an old one, but in case someone else finds this question later)
I think what you want is the build method. The create method saves the instance in the database (which you said you don't want) but the build method doesn't.
Here's an explanation in the factory-boy docs of build and create.

django-channels add user to multiple groups on connect

So I have my models like this.
class Box(models.Model):
objects = models.Manager()
name = models.CharField(max_length=100)
owner = models.ForeignKey('users.User', on_delete=models.CASCADE)
REQUIRED_FIELDS = [name, owner, icon]
class User(AbstractBaseUser):
objects = BaseUserManager()
email = models.EmailField(unique=True)
username = models.CharField(max_length=32, validators=[MinLengthValidator(2)], unique=True)
avatar = models.ImageField(upload_to='avatars/', default='avatars/default.jpg')
REQUIRED_FIELDS = [username, email]
class Member(models.Model):
objects = models.Manager()
box = models.ForeignKey('uploads.Box', on_delete=models.CASCADE, editable=False)
user = models.ForeignKey('users.User', on_delete=models.CASCADE, editable=False)
roles = models.ManyToManyField('roles.Role', through='roles.MemberRole')
invite = models.ForeignKey('users.Invite', null=True, blank=True, on_delete=models.CASCADE)
REQUIRED_FIELDS = [box, user]
I have a websockets framework with routing like this.
websocket_urlpatterns = [
path('gateway/', GatewayEventsConsumer.as_asgi()),
]
class GatewayEventsConsumer(AsyncWebsocketConsumer):
"""
An ASGI consumer for gateway event sending. Any authenticated
user can connect to this consumer. Users receive personalized events
based on the permissions they have.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
async def connect(self):
user = self.scope['user']
if user.is_anonymous:
# close the connection if the
# user isn't authenticated yet
await self.close()
for member in user.member_set.all():
# put user into groups according to the boxes
# they are a part of. Additional groups would be
# added mid-way if the user joins or creates
# a box during the lifetime of this connection
await self.channel_layer.group_add(member.box.id, self.channel_name)
await self.channel_layer.group_add(self.scope['user'].id, self.channel_name)
await self.accept()
async def disconnect(self, close_code):
for member in self.scope['user'].member_set.all():
# remove user from groups according to the boxes they
# are a part of. Additional groups would be
# removed mid-way if the user leaves or gets kicked
# out of a box during the lifetime of this connection
await self.channel_layer.group_discard(member.box.id, self.channel_name)
await self.channel_layer.group_discard(self.scope['user'].id, self.channel_name)
async def fire_event(self, event: dict):
formatted = {
'data': event['data'],
'event': event['event'],
}
box = event.get('box', None)
channel = event.get('overwrite_channel', None)
listener_perms = event.get('listener_permissions', [])
if not listener_perms or not box:
# box would be none if the event was user-specific
# don't need to check permissions. Fan-out event
# directly before checking member-permissions
return await self.send(text_data=json.dumps(formatted))
member = self.scope['user'].member_set.get(box=box)
if listener_perms in member.get_permissions(channel):
# check for permissions directly without any extra context
# validation. Because data-binding is outbound permission
# checking is not complex, unlike rest-framework checking
await self.send(text_data=json.dumps(formatted))
this is how I send ws messages.
(using django signals)
#receiver(post_delete, sender=Upload)
def on_upload_delete(instance=None, **kwargs) -> None:
async_to_sync(channel_layer.group_send)(
instance.box.id,
{
'type': 'fire_event',
'event': 'UPLOAD_DELETE',
'listener_permissions': ['READ_UPLOADS'],
'overwrite_channel': instance.channel,
'box': instance.box,
'data': PartialUploadSerializer(instance).data
}
)
The api needs to send box-specific events, so I have different groups for boxes.
Users which connect to these groups will receive the events they need.
So, when the user connects to the "gateway", I add the user to all the boxes they are a part of, (plus a private group to send user-specific information)
On disconnect, I remove them from the same.
However, I am facing issues here.
An example,
when an user joins a box during the scope of the connection, they would not receive the events that are being sent for that particular box.
when an user leaves a box during the scope of the connection, they would still receive events from that particular box.
Any ways to fix these issues?
relevant github discussion is here.
You can do this by adding 2 handlers similar to fire-event.
The first one adds a user to a group
The second one deletes a user from a group.
Then using Django Signals, send a websocket message to those handlers whenever a user becomes a box member or leaves the box

Django prefetch_related is taking to much runtime

I need to list all my devices. To do this I use a prefetch related to reduce the amount of queries. But one of them is consuming to much time.. I wonder if it can't go better.
I will start with the model construction: I want a list of devices. This is the device model:
class Device(models.Model):
name = models.CharField(max_length=250, null=True, blank=True)
def get_active_gateway(self):
from backend.gateways.models import Gateway
all_gatewaydevices = self.gatewaydevices.all()
for gd in all_gatewaydevices:
if not gd.end_date:
return gd.gateway
return None
In the real code the model is larger, but that code is irrelevant. As you can see, a device has some gatewaydevices (which is a model between gateway and device)
The gatewaydevice model looks like:
class GatewayDevice(models.Model):
gateway = models.ForeignKey(
Gateway, on_delete=models.CASCADE, related_name="devices"
)
device = models.ForeignKey(
Device, on_delete=models.CASCADE, related_name="gatewaydevices"
)
So in my list of devices, I want for every device, the linked gateway.
This is my view:
class AdminDeviceView(GenericAPIView):
def get_permissions(self):
return IsAuthenticated()
# noinspection PyMethodMayBeStatic
def get_serializer_class(self):
return AdminDeviceInfoSerializer
#swagger_auto_schema(
responses={
200: openapi.Response(
_("Successfully fetched all data from devices."),
AdminDeviceInfoSerializer,
)
}
)
def get(self, request):
devices = (
Device.objects.prefetch_related(
"gatewaydevices__gateway",
)
.all()
)
serializer_class = self.get_serializer_class()
serializer = serializer_class(devices, many=True)
devices_data = serializer.data
return Response(
{"total": devices.count(), "items": devices_data}, status=status.HTTP_200_OK
)
This is the part of the serializer that is important:
#staticmethod
def get_gateway(device):
gateway = device.get_active_gateway()
return GatewaySimpleSerializer(gateway).data if gateway else None
Can this get any faster/more efficient?
get_active_gateway is very inefficient - it check each object separately. And when called in get_gateway - it does not utilize prefetch bonuses.
You can filter and get only valid Gateway objects directly in prefetch.
Also using to_attr to map result to same field name as in serializer.
(*not tested)
class AdminDeviceView(GenericAPIView):
# ...
def get(self, request):
# ...
devices = Device.objects.prefetch_related(
Prefetch(
"gatewaydevices",
queryset=Gateway.objects.filter(
devices__end_date__isnull=True
).order_by().distinct(), # distinct in case there are many
to_attr="gateway",
)
)
# ...
class AdminDeviceInfoSerializer(serializers.ModelSerializer):
gateway = GatewaySimpleSerializer(many=True, read_only=True)
# ...

What's the best way to mock a Django User?

I'm trying to instantiate a User instance through a Mock. That mock instance is being passed to another model Profile where I'm checking for any validation errors when a cleaning method is called.
However I'm getting: AttributeError: Mock object has no attribute '_state'
There is this previous post: How to mock users and requests in django. Nevertheless, I want to avoid any database calls.
What can be done differently so that a Mock will in this case?
#models.py
class Profile(models.Model):
hobby = "Hobbyist"
develop = "Developer"
coding_level = (
(hobby, hobby),
(develop, develop)
)
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE
)
birth = models.DateField(verbose_name="Date Of Birth")
coding_level = models.CharField(
verbose_name="Experience",
max_length=20,
choices=coding_level, default=hobby, blank=False
)
bio = models.TextField(
verbose_name="User Bio",
validators=[MinValueValidator(10)]
)
github = models.URLField(
verbose_name="GitHub link",
validators=[check_submitted_link],
unique=True
)
avatar = models.ImageField(upload_to="images/%Y/%m/%d/")
#test_models.py
class TestProfile__001(SimpleTestCase):
def setUp(self):
self.test_user = Mock(
spec=User,
username="test_username",
email="test#email.com"
)
self.profile_data = {
'user': self.test_user,
'birth': '2019-10-07',
'coding_level': 'hobbyist',
'bio': "",
'github': "http://www.test.com",
'avatar': "image.txt"
}
def test_create_profile_fail(self):
with self.assertRaises(ValidationError):
test_profile = Profile(**self.profile_data)
test_profile.clean_fields()
I would argue that there's no point in testing clean_fields, since it's already a part of very well tested Django codebase. But if you insist on testing it, you are definitely should not mock the User out.
Let's look at the code you are trying to test here (this is an excerpt from clean_fields):
raw_value = getattr(self, f.attname)
if f.blank and raw_value in f.empty_values:
continue
try:
setattr(self, f.attname, f.clean(raw_value, self))
except ValidationError as e:
errors[f.name] = e.error_list
We see that it runs through every field on the model trying to call its clean method (source):
def clean(self, value):
"""
Validate the given value and return its "cleaned" value as an
appropriate Python object. Raise ValidationError for any errors.
"""
value = self.to_python(value)
self.validate(value)
self.run_validators(value)
return value
OneToOneField itself does not introduce any of these methods, it's done higher in the class hierarchy, in ForeignKey class. And here's the main part of the validate method:
using = router.db_for_read(self.remote_field.model, instance=model_instance)
qs = self.remote_field.model._default_manager.using(using).filter(
**{self.remote_field.field_name: value}
)
qs = qs.complex_filter(self.get_limit_choices_to())
if not qs.exists():
raise exceptions.ValidationError(
self.error_messages['invalid'],
code='invalid',
params={
'model': self.remote_field.model._meta.verbose_name, 'pk': value,
'field': self.remote_field.field_name, 'value': value,
}, # 'pk' is included for backwards compatibility
)
As you can see, the whole validation consists of nothing but a query construction with a database call! So if you are trying to avoid hitting the database, you should just skip the validation of any foreign keys altogether:
def test_create_profile_fail(self):
with self.assertRaises(ValidationError):
test_profile = Profile(**self.profile_data)
test_profile.clean_fields(exclude=['user']) # don't forget to explain your decision in the comments
So to conclude, the best way to mock the User is to actually create it in the database. If you want to avoid the boilerplate, you can use the factory_boy package.

Django 1.4 Modifying Custom Account Model for Uniqueness of E-mail Addresses

I've already defined a custom user account that utilizes several built in's from the auth User model and, using the user link, links these with some additional fields that I needed to register a user on the database.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
relevant from my models.py
# additional model to incorporate our custom fields to the auth user model
class Account(models.Model):
user = models.OneToOneField(User) #link (pointer) to the users other information in User model
birthdate = models.DateField(blank = True, ) # True makes this field optional
gender = models.CharField(max_length = 1, choices = GENDER_CHOICE, null = True, blank = True)
def __unicode__(self): # define a unicode for the user to access
return u'%s %s' % (self.user.first_name, self.user.last_name) # return first and last name in shell
# custom form to gather information from the user for a new account
class UserRegistration(UserCreationForm):
#class RegistrationForm(forms.ModelForm):
class Meta:
model = User
fields = ("first_name", "last_name", "email", "username", "password1", "password2",)
# ensures uniqueness of user email addresses when registering
def clean_email(self):
print "In custom creation"
email = self.cleaned_data.get(email = 'email')
username = self.cleaned_data.get(username = 'username')
# checks if email address already exists
if User.objects.filter(email__iexact = self.cleaned_data['email']):
print "Email exists"
# if email and User.objects.filter(email__iexact = email).exclude(username=username).exists():
raise forms.ValidationError(u'Email Address is currently used by another user.')
return email
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
relevant from views.py
def Main(request):
if request.user.is_authenticated():
latest_events = Event.objects.all().order_by('-created')[:10] # Returns latest 10 events
my_events = Event.objects.filter(creator=request.user)[:10] # Returns up to 10 events created by current User
my_calendars = Calendar.objects.filter(creator=request.user) # Returns all calendars created by the user
authForm = None
loginForm = None
headerType = "header.html"
else:
latest_events = None
my_events = None
my_calendars = None
headerType = "header_main.html"
authForm = UserRegistration(request.POST or None)
print "Creating account UserRegistration" # TESTING PRINT
print "User email = %s " %(User._meta.get_field('email'))
if request.method == 'POST':
if authForm.is_valid():
newUser = authForm.save(commit=False)
newUser.save()
newUser = authenticate(username=request.POST['username'], password=request.POST['password1'])
login(request, newUser)
return HttpResponseRedirect('/signup/')
....
....
more code on success redirection
....
....
~~~~~~~~~~~~~~~~~~~~~~~~~~~
(I hope I didn't post too much code, just wanted to be thorough)
As you can see there are a few commented out attempts I've made recently. I tried using thee built in RegistrationFormUniqueForm() by downloading 'registration' but I don't actually want to make a new registration form since I already have a working one.
I moved on to trying another suggestion, the code under the comment
# custom form to display additional sign up information
When I tried registering a new user with an already registered email it did not throw any error and allowed the registration. I also tried changing the users email and it allowed the change to an already taken email with no objection.
Can anyone suggest a method for making user registration maintain a unique lock on each individual attempting to register with an email address that may already be taken? As well as preventing them from changing their email to one that is taken by a current user?
Thanks in advance.
EDIT: I made changes to the Models registration form def clean_email() and the def in views to reflect what I currently have that still does not work.
The indentation of your clean_email methods is wrong for both forms. At the moment, they are methods of the Meta class, so will never be called. For example, the registration form should be:
class RegistrationForm(UserCreationForm):
#class RegistrationForm(forms.ModelForm):
class Meta:
model = User
fields = ("first_name", "last_name", "email", "username", "password1", "password2",)
def clean_email(self):
"""ensures uniqueness of user email addresses when registering"""
email = self.cleaned_data.get('email')
This might not be the real problem -- it's easy to get the indentation wrong when pasting code into stack overflow. If that's the case, I'll delete the answer.