I have a foreign key on my models like Patient, and Doctor, which point to a Clinic class. So, the Patient and Doctor are supposed to belong to this Clinic alone. Other Clinics should not be able to see any detail of these Models.
The models look like this:
class Clinic(models.Model):
clinicid = models.AutoField(primary_key=True, unique=True)
name = models.CharField(max_length=60, unique=True)
label = models.SlugField(max_length=25, unique=True)
email = models.EmailField(max_length=100, default='')
mobile = models.CharField(max_length=15, default='')
...
class Doctor(models.Model):
# Need autoincrement, unique and primary
docid = models.AutoField(primary_key=True, unique=True)
name = models.CharField(max_length=200)
username = models.CharField(max_length=15)
regid = models.CharField(max_length=15, default="", blank=True)
...
linkedclinic = models.ForeignKey(Clinic, on_delete=models.CASCADE)
class Patient(models.Model):
cstid = models.AutoField(primary_key=True, unique=True)
date_of_registration = models.DateField(default=timezone.now)
name = models.CharField(max_length=35, blank=False)
ageyrs = models.IntegerField(blank=True)
agemnths = models.IntegerField(blank=True)
dob = models.DateField(null=True, blank=True)
...
linkedclinic = models.ForeignKey(Clinic, on_delete=models.CASCADE)
class UserGroupMap(models.Model):
id = models.AutoField(primary_key=True, unique=True)
user = models.ForeignKey(
User, related_name='target_user', on_delete=models.CASCADE)
group = models.ForeignKey(UserGroup, on_delete=models.CASCADE)
clinic = models.ForeignKey(Clinic, on_delete=models.CASCADE)
...
From my Vue app, I will post using Axios to the django app which uses DRF, and thus get serialized data of Patients and Doctors. It all works fine if I try to use the following sample code in function view:
#api_view(['GET', 'POST'])
def register_patient_vue(request):
if request.method == 'POST':
print("POST details", request.data)
data = request.data['registration_data']
serializer = customerSpecialSerializer(data=data)
if serializer.is_valid():
a = serializer.save()
print(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
print("Serializer is notNot valid.")
print(serializer.errors)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Sample output:
POST details {'registration_data': {'name': 'wczz', 'ageyrs': 21, 'agemonths': '', 'dob': '', 'gender': 'unspecified', 'mobile': '2', 'email': '', 'alternate': '', 'address': '', 'marital': 'unspecified', 'city': '', 'occupation': '', 'linkedclinic': 10}}
data: {'name': 'wczz', 'ageyrs': 21, 'agemonths': '', 'dob': '', 'gender': 'unspecified', 'mobile': '2', 'email': '', 'alternate': '', 'address': '', 'marital': 'unspecified', 'city': '', 'occupation': '', 'linkedclinic': 10}
However, I need to authenticate the request by special custom authentication. I have another class called UserGroupMap which has Foreign Keys for both User and Clinic, so that if there is a match for a filter for the clinic and user, in the map, it will authenticate. Else it should fail authentication and the data should not be retrieved or serializer saved.
In my previous simple pure django project I used to employ a custom permission function, and decorating my view with it:
#handle_perm(has_permission_level, required_permission='EDIT_CLINICAL_RECORD', login_url='/clinic/')
def some_function(request, dept_id):
....
Some code which runs after authentication
And it would use the following:
def handle_perm(test_func, required_permission=None, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
"""
Decorator for views that checks that the user passes the given test,
redirecting to the log-in page if necessary. The test should be a callable
that takes the user object and returns True if the user passes.
"""
def decorator(view_func):
#wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
print(f"Required permission level is {required_permission}")
if has_permission_level(request, required_permission):
print("User has required permission level..Allowing entry.")
return view_func(request, *args, **kwargs)
print("FAILED! User does not have required permission level. Access blocked.")
path = request.build_absolute_uri()
resolved_login_url = resolve_url(login_url or settings.LOGIN_URL)
# If the login url is the same scheme and net location then just
# use the path as the "next" url.
login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
current_scheme, current_netloc = urlparse(path)[:2]
if ((not login_scheme or login_scheme == current_scheme) and
(not login_netloc or login_netloc == current_netloc)):
path = request.get_full_path()
from django.contrib.auth.views import redirect_to_login
return redirect_to_login(
path, resolved_login_url, redirect_field_name)
return _wrapped_view
return decorator
def has_permission_level(request, required_permission, clinic=None):
print("has_permission_level was called.")
user = request.user
print(f'user is {user}')
clinic=clinic_from_request(request)
print(f"has_permission_level called with clinic:{clinic}")
if clinic is None:
print("clinic is none")
return HttpResponseRedirect('/accounts/login/')
group_maps = UserGroupMap.objects.filter(user=user, clinic=clinic)
print(f"No: of UserGroupMap memberships: {len(group_maps)}")
if len(group_maps) < 1:
# There are no UserGroupMap setup for the user. Kindly set them up.\nHint:Admin>Manage users and groups>Users
return False
# Now checking Group memberships whether the user has any with permisison
for map in group_maps:
rolesmapped = GroupRoleMap.objects.filter(group=map.group)
if len(rolesmapped) < 1:
print(f"No permission roles.")
else:
for rolemap in rolesmapped:
print(f"{rolemap.role}", end=",")
if rolemap.role.name == required_permission:
print(
f"\nAvailable role of [{map.group}] matched required permission of [{required_permission}] in {clinic.name} [Ok]")
return True
return False
I need to build a custom authentication using DRF, so that it reads the POSTed data, and checks the linkedclinic value, and employs similiar logic.
I started like this:
def has_permission_POST(request, required_permission, clinic=None):
print("has_permission_POST was called.")
user = request.user
print(f'user is {user}')
if request.method == 'POST':
print(request)
print(dir(request))
print("POST details: POST:", request.POST, "\n")
print("POST details: data:", request.data, "\n")
....
# Further logic to check the mapping
return True
else:
print("Not a valid POST")
return Response("INVALID POST", status=status.HTTP_400_BAD_REQUEST)
# And decorating my DRF view:
#handle_perm(has_permission_POST, required_permission='EDIT_CLINICAL_RECORD', login_url='/clinic/')
#api_view(['GET', 'POST'])
def register_patient_vue(request):
if request.method == 'POST':
print("POST details", request.data)
data = request.data['registration_data']
The problem is that if I run this, then, has_permission_POST cannot get the value of request.data, which contains the data posted from my frontend. I can work around this, by adding the #api_view(['GET', 'POST']) decorator to has_permission_POST. But that introduces another error, a failed assertion:
AssertionError: Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` to be returned from the view, but received a `<class 'bool'>`
This happens from has_permission_POST once it is decorated with #api_view.
So my problems:
How to implement a custom authentication for my use case?
If I am going about this right, by using this custom has_permission_level, how can I get the request.data in this function before my actual api view is called, so that I can read the clinic id and do the checks for permission that I need.
I have taken a look at the CustomAuthentication provided by DRF, but could not find out how to get the request.data parameters in the custom class.
Thanks to #MihaiChelaru, I was able to find a solution to my problem.
I created a custom Permission class by extending permissions.BasePermission, and using my custom logic in the special has_permission function. I went a step further and implemented checking of Token from the request. Once token is authenticated, the user can be got from the matching token from the Token table. I found that in the custom permission class, I could read the full request.data paramter passed by Vue and Postman. Once I read that, I could easily implement the custom checking of User permissions that my custom models had.
class CustomerAccessPermission(permissions.BasePermission):
message = 'No permission to create new patient records'
def has_permission(self, request, view):
bearer_authorizn = request.META.get('HTTP_AUTHORIZATION')
try: #Different apps like POSTMAN, and Vue seem to use different strings while passing token
token = bearer_authorizn.split("Bearer ")[1]
except Exception as e:
try:
token = bearer_authorizn.split("Token ")[1]
except Exception as e:
raise NotAuthenticated('Did not get token in request')
try:
token_obj = Token.objects.get(key=token)
except self.model.DoesNotExist:
raise AuthenticationFailed('Invalid token')
if not token_obj.user.is_active:
raise AuthenticationFailed('User inactive or deleted')
print("Username is %s" % token_obj.user.username)
print("POST details", request.data)
linkedclinic_id = request.data['data']['linkedclinic']
clinic = Clinic.objects.get(clinicid=int(linkedclinic_id))
print("Clinic membership requested:", clinic)
group_maps = UserGroupMap.objects.filter(user=user, clinic=clinic)
print(f"No: of UserGroupMap memberships: {len(group_maps)}")
if len(group_maps) > 1:
return True
return False
#api_view(['POST'])
#permission_classes([CustomerAccessPermission])
def register_patient_vue(request):
logger.info('In register_patient_vue...')
...
I am using DRF, DRF-JWT, Allauth and Res-auth, and djangorestframework-jwt-refresh-token in my Django Application.
I have a custom JWT Register Serializer to collect some additional user info and create and create a refresh-token that is used to refresh expired JWT tokens. We have that working across back-end and iOS Application with no problems for email signup. I am now trying to implement the JWT with the sociallogin element of allauth in particular Facebook as a provider.
I can create a refresh token against a Facebook user by overriding the DefaultSocialAccountAdapter but I'm struggling to return a JSON response with a JWT with said refresh token to mobile client.
This creates refresh token:
class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
def save_user(self, request, sociallogin, form):
user = super(CustomSocialAccountAdapter, self).save_user(request, sociallogin, form)
app = 'users'
user.refresh_tokens.create(app=app)
return user
I can create JWT manually with this:
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
I'm just having difficulty putting it all together, should I be overriding the adapter or using pre_social_login signal.
Any pointers appreciated.
I went with the following to return the long lived refresh token along with avatar url on the Facebook Login:
class CustomJWTSerializer(JWTSerializer):
"""
OVERIDE JWTSerializer Base Serializer for JWT authentication to
add long refresh_token to returned JSON
"""
refresh_token = serializers.CharField()
avatar_url = serializers.CharField()
class FacebookLogin(SocialLoginView):
adapter_class = FacebookOAuth2Adapter
def process_login(self):
get_adapter(self.request).login(self.request, self.user)
user = self.request.user
app = 'users'
try:
refresh_token = user.refresh_tokens.get(app=app).key
except RefreshToken.DoesNotExist:
refresh_token = None
if refresh_token == None:
app = 'users'
user.refresh_tokens.create(
app=app
)
preferred_avatar_size_pixels = 256
facebook_social_account = SocialAccount.objects.get(user=user)
uid = facebook_social_account.uid
picture_url = "http://graph.facebook.com/{0}/picture?width={1}&height={1}".format(
uid, preferred_avatar_size_pixels)
profile = Profile(user=user, avatar_url=picture_url)
profile.save()
def get_response(self):
serializer_class = CustomJWTSerializer
refresh_token = RefreshToken.objects.get(user=self.user)
profile = Profile.objects.get(user=self.user)
avatar_url = profile.avatar_url
if getattr(settings, 'REST_USE_JWT', False):
data = {
'user': self.user,
'token': self.token,
'refresh_token': refresh_token,
'avatar_url': avatar_url
}
serializer = serializer_class(instance=data,
context={'request': self.request})
else:
serializer = serializer_class(instance=self.token,
context={'request': self.request})
return Response(serializer.data, status=status.HTTP_200_OK)
I don't know if this was the proper way to do this, but it works for now.
in DRF i have a some custom action that will do something to user model.user instances are all in state of is_active = False.Im trying to make something that turns the user to is_active = True. i made some a token model that has OneToOne to my user model.the function im trying to make is : if token that user put in the form equals to user.token then set user.is_active = True.im confused how to do that. I made my own serializer class :
class ActivateUserSerializer(serializers.ModelSerializer):
phonenumber = serializers.CharField()
token = serializers.CharField()
class Meta:
model = UserProfile
fields = ['phonenumber','token']
def get_token(self, obj):
request = self.context.get('request')
x = request.data['phonenumber']
obj = UserProfile.objects.get(phonenumber=x)
if request.data['token'] == obj.first_token:
obj.is_active = True
obj.save()
i know this is not .create() .or update() function.so this is how I reach so far.I dont know what view i should use for this functionality.
You could create a new POST endpoint in your API in order to get this custom action, for example:
api/users/<phone number>/activate
Then, in the view class, you can implement the action:
from rest_framework import status, viewsets
from rest_framework.decorators import detail_route
from rest_framework.response import Response
class UserView(viewsets.ModelViewSet):
queryset = UserProfile.objects.all()
# Use your own user serializer
serializer_class = UserSerializer
#detail_route(methods=['post', ])
def activate(self, request, phonenumber):
obj = UserProfile.objects.get(phonenumber=phonenumber)
# The POST request expects a token
if not request.data['token']:
return Response({'message': 'Token not provided'},
status=status.HTTP_400_BAD_REQUEST)
# Compare the token
elif request.data['token'] == obj.first_token:
obj.is_active = True
obj.save()
return Response({'message': 'User activated'})
# Maybe you could add an error code if you need
return Response({'message': 'User not activated'})
I'm using Django rest framework. I've written the following view to register a new user to the system.
#api_view(['POST'])
#csrf_exempt
#permission_classes((AllowAny, ))
def create_user(request):
email = request.DATA['email']
password = request.DATA['password']
try:
user = User.objects.get(email=email)
false = False
return HttpResponse(json.dumps({
'success': False,
'reason': 'User with this email already exists'
}), content_type='application/json')
except User.DoesNotExist:
user = User(email=email, username=email)
user.set_password(password)
user.save()
profile = UserProfile(user=user)
profile.save()
profile_serialized = UserProfileSerializer(profile)
token = Token(user=user)
token.save()
return HttpResponse(json.dumps({
'success': True,
'key': token.key,
'user_profile': profile_serialized.data
}), content_type='application/json')
Is there a better, slightly more secure way, of creating a user registration api in DRF that doesn't leave the endpoint so open to sql injection?
Forgive me to digress a little, but I can't help but wonder use could have gotten away with far less code, than you have if you had created a serializer and used a class-based view. Besides, if you had just created email as EmailField of serializer it would have automatically guaranteed the validation of email. Since you are using orm interface, risk of sql injection is much less than raw query in my opinion.
Sample Code:-
class UserList(CreateAPIView):
serializer_class = UserSerializer
class UserSerializer(ModelSerializer):
email = serializers.EmailField()
raw_password = serializers.CharField()
Something on these lines, Obviously I couldn't write entire code.
You could validate the email in your serializer using the validate-email-address module like this:
from validate_email_address import validate_email
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
def validate_email(self, value):
if not validate_email(value):
raise serializers.ValidationError("Not a valid email.")
return value
class Meta:
model = User
fields = ('email', 'password')
Also, you might consider a packaged auth/registration solution like Djoser.
How would I add the auth token to the userSeralizer?
This is my serializer:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username')
And then in my views the url:
#api_view(['POST', 'DELETE'])
def create_user(request):
"""
API endpoint to register a new user
"""
model = User
serializer_class = UserSerializer
username, password = request.POST['username'], request.POST['password']
try:
user = User.objects.create_user(username, username, password)
except IntegrityError:
user = User.objects.get(username=username, email=username)
# the users token, we will send this to him now.
token = Token.objects.get(user=user)
if request.method == "POST":
serializer = UserSerializer(user)
return Response(data)
I think it would be nice to have the token in the serializer, or not?
From a security standpoint, auth tokens should not be passed around in the serializer. If your User view can be seen by anyone, then anyone could to impersonate any user without much trouble.
Tokens are meant to be returned only after successful login, not when an user is created. This is why most sites require Users to sign in just after the account was created.
But for the sake of the question, there are several ways to add items to serializers.
First, is a little hacky but doesn't require custom models
# Not adding context would raise a DeprecationWarning in the console
serializer = UserSerializer(user, context={'request': request})
data = serializer.data
data['token'] = token
return Response(data)
Last but not least, is a bit more elegant but requires a custom User class. However you could use it in your app models.
# in models.py inside your User model
def get_my_token(self):
return Token.objects.get(user=user)
my_token = property(get_my_token)
and then in the serializer class add the field with the token (remember to add it to the fields attribute in your meta class)
class UserSerializer(serializers.ModelSerializer):
token = serializers.Field(source='my_token')
class Meta:
model = User
fields = ('id', 'username', 'token')