I am using custom authentication in Django-Tastypie, which returns the username and password when authentication succeeds. In the ModelResource instance, once the authentication is successful, the username and password are available:
class TestResource(ModelResource):
class Meta:
queryset = Test.remote_supported.get_all(username,password)
resource_name = 'test'
filtering = {
'id' : ALL,
}
detail_allowed_methods = ['get', 'post', 'patch']
authorization = Authorization()
authentication = MyCustomAuth()
always_return_data = True
def __init__(self, api_name=None):
self.username = None
self.password = None
super(TestResource, self).__init__(api_name)
def is_authenticated(self, request):
auth_result = self._meta.authentication.is_authenticated(request)
if isinstance(auth_result, HttpResponse):
raise ImmediateHttpResponse(response=auth_result)
if not auth_result is True:
raise ImmediateHttpResponse(response=http.HttpUnauthorized())
# this is where I receive the username and password from my custom auth
self.username, self.password = self._meta.authentication.get_credentials()
This code obviously does not work, because that username and that password are not available in the Metaclass, and even if it were, changing it would not affect only this instance, but all instances, which is not my intention, because the scope of username and password should be per RESTful query.
This is what my model looks like:
class RemoteAuthSupported(models.Manager):
def get_all(self, username, password):
# ... here I do some custom operations with the username and password
return super(RemoteAuthSupported, self).get_query_set()
class Test(models.Model):
objects = models.Manager()
remote_supported = RemoteAuthSupported()
# ... the field declarations follow here ... #
The reason I am trying to do this is that I am using a non-ORM data source with my Django application, which requires authentication of its own, but with the same username and password. What would be the method of handling this parameter passing from a Tastypie ModelResource to a Django Model? I probably should mention here that there is no user model in use.
What you could do is override the obj_get_list method that returns the list of resources for a request. Also, you could set the username and password to the request object itself, so the paramters get carried over the request-response path. Also, you'd need to set the queryset to all().
def obj_get_list(self, bundle, **kwargs):
original = super(TestResource, self).obj_get_list(bundle, **kwargs)
request = bundle.request
return original.get_all(request.username, request.password)
Or the other way round - you could add custom authorization that would filter the objects list. The request attributes part still holds.
class MyAuth(Authorization):
def authorized_read_list(self, objects, bundle):
request = bundle.request
return objects.get_all(request.username, request.password)
If you would rather imitate queryset using get_all than just alternating list endpoint, you could override the get_object_list.
def get_object_list(self, request):
original = super(TestResource, self).get_object_list(request)
return original.get_all(request.username, request.password)
I looked into the Tastypie documentation, and it seems like the following could be a solution, albeit amateurish:
class TestResource(ModelResource):
class Meta:
queryset = Test.remote_supported.get_all('dummyusername','dummypassword')
resource_name = 'test'
filtering = {
'id' : ALL,
}
detail_allowed_methods = ['get', 'post', 'patch']
authorization = Authorization()
authentication = MyCustomAuth()
always_return_data = True
def get_object_list(self, request):
"""
This method calls a clone of the queryset declared in the Metaclass.
It is called every time a query is executed.
Here the actual username and password is passed to the model.
"""
return Site.remote_supported.get_all(self.username,self.password)
The problem was that the queryset declaration happened in the Metaclass only once, not per query, so the username and password acquired from the http request could not be passed on to the query in the Meta level. But since get_object_list() clones the declaration and performs the actual call with the updates argument values, this gets the job done. It is working for me, but I feel there should be a better solution for doing this.
Related
I am developing a Flask API using Flask Restful. I wonder if there is a clean way for authorizing users that would not force me to code duplication. I use Flask-JWT-Extended for authentication in my API.
I have got some endpoints that I want to be accessible only by user with admin role OR the user, that is related to a given resource.
So, let's say I'd like to enable user to obtain information about their account and I'd like to prevent other users from accessing this information. For now, I am solving it this way:
from flask import request, Response
from flask_restful import Resource, reqparse
from flask_jwt_extended import (create_access_token, create_refresh_token, jwt_required, get_jwt_identity)
from app.mod_auth.models import User
[...]
class UserApi(Resource):
#jwt_required()
def get(self, name):
current_user = User.find_by_login(get_jwt_identity())
if current_user.login==name or current_user.role==1:
user = User.query.filter_by(login=name).first_or_404(description="User not found")
return user.json()
else:
return {'message': 'You are not authorized to access this data.'}, 403
[...]
So first I check, if there's correct and valid JWT token in the request, and then, basing on the token I check if the user related with the token is the same, as the user, whose data is being returned. Other way for accessing data is user with role 1, which I treat as an administrative role.
This is the part of my User's model:
[...]
class User(Base):
login = db.Column(db.String(128), nullable=False)
email = db.Column(db.String(128), unique=True, nullable=False)
password = db.Column(db.String(192), nullable=False)
active = db.Column(db.Boolean(), default=True, nullable=False)
role = db.Column(db.SmallInteger, default=0, nullable=False)
[...]
Of course, soon I'll have a few endpoints with data specific for user.
I have found an example of custom operator in Flask-JWT-Extended, that provides an authorization for admin users: flask-jwt-extended admin authz - but on the other hand, it does not support user-specific authorization. I have no idea, how to improve that snippet in order to verify, if the user requesting for an resource is a specific user with rights to the resource.
How can I define a custom operator, that will provide correct access to the user-specific data?
Maybe I should include some kind of owner data in each DB model, that should support authorization, and verify that in the requests, as in the example above?
Thanks in advance
You can create a decorator that checks the identity of the user:
def validate_user(role_authorized:list() = [1]):
def decorator(fn):
#wraps(fn)
def wrapper(*args, **kwargs):
name = request.path.rsplit('/', 1)[-1]
current_user = User.find_by_login(get_jwt_identity())
if (current_user.login == name) or (current_user.role in role_authorized):
kwargs["logged_user"] = current_user # If you need to use the user object in the future you can use this by passing it through the kwargs params
return fn(*args, **kwargs)
else:
return {'message': 'You are not authorized to access this data.'}, 403
return wrapper
return decorator
class Test(Resource):
#jwt_required()
#list of the roles authorized for this endpoint = [1]
#validate_user([1])
def post(self, name, **kwargs):
#logged_user = kwargs["logged_user"] # Logged in User object
user = User.query.filter_by(login=name).first_or_404(description="User not found")
return user.json()
I have question related with Django .
I am using Knox Token Authentication to generate tokens for every user when he log in on the page.
Now I want to use that token for every request that will send so I can get the corresponding user for the token. Also I am using custom function example def dashboard(request) in Django for every URL route.
I have see on youtube that there are option to get user from token but is not with functions
class UserAPI(generics.RetrieveAPIView):
permission_classes = [
permissions.IsAuthenticated,
]
serializer_class = UserSerializer
def get_object(self):
return self.request.user
So is there a whey to get the corresponding user from a token within a custom function
Great, I figured out in hours that knox doesn't come with full token_key stored in database.
Real token we can get is something like:
a512529e7ffceaa8406ceb616d088b3422ad15811a5eb470e8f4c4896c9aa649
In database token_key is stored by default a512529e. 8 digits.
Filter objects using this:
knox_object = AuthToken.objects.filter(token_key__startswith=token[:8]).first()
Then get user object
knox_object.user.username
Or you can use this, faster
from knox.settings import CONSTANTS
knox_object = AuthToken.objects.filter(token_key=token[:CONSTANTS.TOKEN_KEY_LENGTH]).first()
From the knox source codes
class CONSTANTS:
'''
Constants cannot be changed at runtime
'''
TOKEN_KEY_LENGTH = 8
DIGEST_LENGTH = 128
SALT_LENGTH = 16
def __setattr__(self, *args, **kwargs):
raise Exception('''
Constant values must NEVER be changed at runtime, as they are
integral to the structure of database tables
''')
CONSTANTS = CONSTANTS()
You can see TOKEN_KEY_LENGTH is of 8 digits.
I wrote a simple function to do that
from knox.models import AuthToken
from knox.settings import CONSTANTS
def get_user_from_token(token):
objs = AuthToken.objects.filter(token_key=token[:CONSTANTS.TOKEN_KEY_LENGTH])
if len(objs) == 0:
return None
return objs.first().user
Life be easier now. :)
Yes, I improved it and published it.
You may try my fork. If you just simply want to add #smart_token_user before any GET/POST/PUT/... methods.
https://github.com/xros/django-rest-knox
Just git clone, and pip install ./
I wrote a decorator.
With this,
in our app views.py
we can easily get user object by doing so,#smart_token_user will modify the request handler. We can have a request.user attr only once the token is valid. And all invalid attempts will be thrown out with HTTP 401 Unauthorized response.
Life can be easier with this decorator.
from knox.models import smart_token_user
class CheckUserEmail(generics.RetrieveAPIView):
permission_classes = (IsAuthenticated,)
#smart_token_user
def get(self, request):
return Response({
"username": request.user.username,
"email": request.user.email,
"password": request.user.password,
}, status=status.HTTP_200_OK)
Or use this like original if you want: authentication_classes = (TokenAuthentication,)
class CheckUserEmail(generics.RetrieveAPIView):
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
def get(self, request):
return Response({
"username": request.user.username,
"email": request.user.email,
"password": request.user.password,
}, status=status.HTTP_200_OK)
I'm trying to run a test of the middleware of a django app. Looks like this:
class TestAuthenticationMiddleware(TestCase):
#classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user('test_user', 'test#example.com', 'test_password')
def setUp(self):
self.middleware = AuthenticationMiddleware(lambda req: HttpResponse())
self.client.force_login(self.user)
self.request = HttpRequest()
self.request.session = self.client.session
But I'm getting this attribute error:
queryset = user.get_subscriptions_all()
AttributeError: 'User' object has no attribute 'get_subscriptions_all'
because the User class is defined like this:
User(AbstractBaseUser, LimitHelpers):
[...]
def get_subscriptions_all(self):
return self.subscriptions.all().union(self.account.subscriptions.all())
and on my utils.py:
#receiver(user_logged_in)
def callback_user_loggedin(sender, request, user, **kwargs):
if not user.is_staff and not user.is_superuser:
# Activating user's language
saveCodeCountryFromUser(request, user)
# Updating subscriptions
queryset = user.get_subscriptions_all()
for sub in queryset:
cls = get_subscription_class(sub)
if cls is not None:
cls.callback_user_loggedin()
Any idea about how to use the create_user to include the get_subscriptions_all attr?
You must set the setting AUTH_USER_MODEL = 'myapp.models.User'
See: substituting a custom user model.
Without this setting, Django will continue to use the default builtin User. You should also ensure your tests also have this configuration (if you use a separate config for tests)
I need to create JWT token authentication, but I don't know how, could you explain me how to do it better or put some examples?
my view:
class UserLogin(generics.CreateAPIView):
"""
POST auth/login/
"""
# This permission class will overide the global permission
# class setting
permission_classes = (permissions.AllowAny,)
queryset = User.objects.all()
serializer_class = TokenSerializer
def post(self, request, *args, **kwargs):
username = request.data.get("username", "")
password = request.data.get("password", "")
user = auth.authenticate(request, username=username, password=password)
if user is not None:
auth.login(request, user)
return Response({
"token": jwt_encode_handler(jwt_payload_handler(user)),
'username': username,
}, status=200)
return Response(status=status.HTTP_401_UNAUTHORIZED)
You are creating the token in that view. After that, you need two other mechanism in place:
Your client should send this token the the API with each request, in the Authorization header, like:
Authorization: Bearer your_token
On the api side, you need to use an authentication class, that looks for Authorization header, takes the token and decodes it, and finds the user instance associated with the token, if the token is valid.
If you are using a library for drf jwt authentication, it should have an authentication class that you can use. If you are implementing it manually, you need to write an authentication class that subclasses DRF's BaseAuthentication class yourself. It could basically look like this:
class JwtAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
auth_header = request.META.get('HTTP_AUTHORIZATION')
if auth_header:
key, token = auth_header.split(' ')
if key == 'Bearer':
# Decode the token here. If it is valid, get the user instance associated with it and return it
...
return user, None
# If token exists but it is invalid, raise AuthenticationFailed exception
# If token does not exist, return None so that another authentication class can handle authentication
You need to tell DRF to use this authentication class. Add this to your settings file for that:
REST_FRAMEWORK = {
...
'DEFAULT_AUTHENTICATION_CLASSES': [
'path.to.JwtAuthentication',
...
]
}
How to get the session_key in form Class? It is not possible to get the request parameter to do something like this : request.user
I've got this situation, and I need to pass to function get_user session_key which is also
not possible to retrieve from request.
class CustomEntryAdminForm(EntryAdminForm):
def get_user(session_key):
session = Session.objects.get(session_key=session_key)
uid = session.get_decoded().get('_auth_user_id')
user = User.objects.get(pk=uid)
return user
categories = MPTTModelMultipleChoiceField(
label=_('Categories'), required=False,
queryset=Category.objects.filter(groups__in=get_user('uwry5olhemchxnmwa36v10zt2bg9zeci').groups.all()),
widget=MPTTFilteredSelectMultiple(_('categories'), False,
attrs={'rows': '10'}))
Use pass user as keyword argument to your form. You do not need to jump through hoops and load active session key from request and then load user from decoded session. All you need to do is:
in view:
myform = MyFormClass(user= request.user)
in form:
class MyFormClass(forms.Form):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super(MyFormClass, self).__init__(*args, **kwargs)
self.fields['categories'].queryset = Category.objects.filter(groups__in = self.user.groups.all())
NB! not complete working code. I just wanted to show you how you can use the self.user in queryset.