Accessing a SerializerMethodField in create() - python

I want to create a password on server side and send it back to the user. Below is the code I have written:
class ASCreateSerializer(serializers.Serializer):
name = serializers.CharField(write_only = True)
password = serializers.SerializerMethodField()
def get_password(self, obj):
from django.utils.crypto import get_random_string
password = get_random_string(length=16)
return password
def create(self, validated_data):
name = validated_data['name']
as = AS.objects.get_or_create(name = name,
password = validated_data['password']
)
I am getting a key error for 'password'. How can I access the value of a SerializerMethodField in create()?

SerializerMethodField (Doc) is a read-only field. This field is used when you want to compute the field for a given object and thus would not be necessary if you are receiving the input (write case). So, in you case you can generate password inside the create method and have only name in the serializer.

You can access the get_password() method by calling self.get_password() as #Andrey Shipilov mentioned. But remove the unnecessary second argument obj from definition of get_password().
If what you require is to save a random password to database and return it in response, consider doing something like:
def create_password():
from django.utils.crypto import get_random_string
password = get_random_string(length=16)
return password
class ASCreateSerializer(serializers.Serializer):
name = serializers.CharField(write_only = True)
password = serializers.CharField()
def create(self, validated_data):
name = validated_data['name']
as = AS.objects.get_or_create(
name = name,
password = create_password()
)

Related

Returning JsonResponse from a django rest-framework serializer

My question is how would I return a custom JsonResponse from a rest-framework serializer?
I have a view with this code:
# Send money to outside of Pearmonie
class sendmoney_external(viewsets.ModelViewSet):
# Database model
queryset = User.objects.all()
# Serializer - this performs the actions on the queried database entry
serializer_class = SendMoneyExternalSerializer
# What field in database model will be used to search
lookup_field = 'username'
My serializer is something like this:
# Serializer for sending money to another Pearmonie user
class SendMoneyExternalSerializer(serializers.ModelSerializer):
# This is the function that runs for a PUT request
def update(self, instance,validated_data):
# Verifies the requesting user owns the database account
if str(self.context['request'].user) != str(instance.username) and not str(self.context['request'].user) in instance.userprofile.banking_write:
raise exceptions.PermissionDenied('You do not have permission to update')
account_number = self.context['request'].data['to_account']
amount = self.context['request'].data['amount']
to_bank = self.context['request'].data['to_bank']
# Creates the order in the database
from_user = BankingTransaction.objects.create(
user = instance,
date = timezone.now(),
from_account_num = instance.userprofile.account_number,
from_account_name = instance.userprofile.company,
to_account = self.context['request'].data['to_account'],
to_bank = to_bank,
amount_transferred = amount,
description = self.context['request'].data['description'],
trans_reference = self.context['request'].data['trans_reference'],
status = self.context['request'].data['status'],
)
instance.userprofile.notification_count += 1
instance.userprofile.save()
# Creates the notification for the supplier in the database
from_notification = Notifications.objects.create(
user=instance,
date=timezone.now(),
read=False,
message=f'You have paid N{amount} to account number {account_number} ({to_bank})'
)
return instance
class Meta:
model = User
fields = ['pk']
This just simple returns the User model's primary key in json... how would I make it return something custom?
I want to make a request to an external api, and send that external api's response in my django's response.
You can use SerializerMethodField for custom logic.
class SendMoneyExternalSerializer(serializers.ModelSerializer):
# This is the function that runs for a PUT request
custom_data = serializers.SerializerMethodField()
class Meta:
model = User
fields = ['pk', 'custom_data']
def get_custom_data(self, obj):
# Your coustom logic here. (obj) represent the User object
return f'{obj.first_name} {obj.username}'

New objects are made every time

I made a single function for register and login with mobile and otp. The register part is in the else part of the function, and the if part is the login function. Every time I log in with the already registered number, it makes a new object in the database, and I don't want that. I want to just update the otp part from when the number was registered in the database.
views.py
class RegistrationAPIView(APIView):
permission_classes = (AllowAny,)
serializer_class = ProfileSerializer
def post(self, request):
mobile = request.data['mobile']
data = Profile.objects.filter(mobile = mobile).first()
if data:
serializer = self.serializer_class(data=request.data)
mobile = request.data['mobile']
if serializer.is_valid(raise_exception=True):
instance = serializer.save()
content = {'mobile': instance.mobile, 'otp': instance.otp}
mobile = instance.mobile
otp = instance.otp
print("Success")
send_otp(mobile,otp)
return Response(content, status=status.HTTP_201_CREATED)
else:
return Response({"Error": "Login in Failed"}, status=status.HTTP_400_BAD_REQUEST)
else:
serializer = self.serializer_class(data=request.data)
mobile = request.data['mobile']
if serializer.is_valid(raise_exception=True):
instance = serializer.save()
content = {'mobile': instance.mobile, 'otp': instance.otp}
mobile = instance.mobile
otp = instance.otp
send_otp(mobile,otp)
return Response(content, status=status.HTTP_201_CREATED)
else:
return Response({"Error": "Sign Up Failed"}, status=status.HTTP_400_BAD_REQUEST)
serializers.py
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = ['mobile']
def create(self, validated_data):
instance = self.Meta.model(**validated_data)
global totp
secret = pyotp.random_base32()
totp = pyotp.TOTP(secret, interval=300)
otp = totp.now()
instance.otp = str(random.randint(1000 , 9999))
instance.save()
return instance
models.py
'''
class Profile(models.Model):
mobile = models.CharField(max_length=20)
otp = models.CharField(max_length=6)
'''
I'm assuming here that you're using DRF Serializers. If that's the case, note from the documentation that :
Calling .save() will either create a new instance, or update an existing instance, depending on if an existing instance was passed when instantiating the serializer class:
# .save() will create a new instance.
serializer = CommentSerializer(data=data)
# .save() will update the existing `comment` instance.
serializer = CommentSerializer(comment, data=data)
As an aside, you probably want to do a get_or_create or create_or_update with defaults instead of having the if/else clause.
Option A: First thing first. If there is only and only one mobile number for each Profile you should add a unique constraint for your Profile model as well. Something like:
class Profile(models.Model):
mobile = models.CharField(max_length=20, unique=True)
otp = models.CharField(max_length=6)
Do not forget to make migration and migrate these new changes to your database (also note that there might be some duplicate records in Profile table so if you're not on production server first delete all of your records and then make this migration).
And then make this change to your serializer's create method:
def create(self, validated_data):
global totp
secret = pyotp.random_base32()
totp = pyotp.TOTP(secret, interval=300)
otp = totp.now()
instance = self.Meta.model.objects.update_or_create(**validated_data, defualts=dict(otp=str(random.randint(1000 , 9999))))[0]
return instance
with update_or_create now you're sure that if the record with specific mobile exists you will update that and if not you will create new one.
Option B: But if you don't want to make this change to your database for any reason you can just simply do this:
def create(self, validated_data):
global totp
secret = pyotp.random_base32()
totp = pyotp.TOTP(secret, interval=300)
otp = totp.now()
if self.Meta.model.objects.filter(**validated_data).exists():
instance = self.Meta.model.objects.filter(**validated_data).last()
instance.otp = str(random.randint(1000 , 9999))
instance.save()
else:
instance = self.Meta.model(**validated_data)
instance.otp = str(random.randint(1000 , 9999))
instance.save()
return instance
Note that there might be multiple records in your table with same mobile number as long as there isn't any constraint on that model and here we are only updating latest record in your Profile table. I hope these two options solve your problem.

Django password hasher using php format of function password_hash()

I have to add a backward-compatible Django application that supports legacy passwords persisted in a database created with the use of PHP function password_hash() which output is like
$2y$10$puZfZbp0UGMYeUiyZjdfB.4RN9frEMy8ENpih9.jOEngy1FJWUAHy
(salted blowfish crypt algorithm with 10 hashing rounds)
Django supports formats with the prefixed name of the algorithm so if I use BCryptPasswordHasher as the main hasher output will be like:
bcrypt$$2y$10$puZfZbp0UGMYeUiyZjdfB.4RN9frEMy8ENpih9.jOEngy1FJWUAHy
I have created custom BCryptPasswordHasher like:
class BCryptPasswordHasher(BasePasswordHasher):
algorithm = "bcrypt_php"
library = ("bcrypt", "bcrypt")
rounds = 10
def salt(self):
bcrypt = self._load_library()
return bcrypt.gensalt(self.rounds)
def encode(self, password, salt):
bcrypt = self._load_library()
password = password.encode()
data = bcrypt.hashpw(password, salt)
return f"{data.decode('ascii')}"
def verify(self, incoming_password, encoded_db_password):
algorithm, data = encoded_db_password.split('$', 1)
assert algorithm == self.algorithm
db_password_salt = data.encode('ascii')
encoded_incoming_password = self.encode(incoming_password, db_password_salt)
# Compare of `data` should only be done because in database we don't persist alg prefix like `bcrypt$`
return constant_time_compare(data, encoded_incoming_password)
def safe_summary(self, encoded):
empty, algostr, work_factor, data = encoded.split('$', 3)
salt, checksum = data[:22], data[22:]
return OrderedDict([
('algorithm', self.algorithm),
('work factor', work_factor),
('salt', mask_hash(salt)),
('checksum', mask_hash(checksum)),
])
def must_update(self, encoded):
return False
def harden_runtime(self, password, encoded):
data = encoded.split('$')
salt = data[:29] # Length of the salt in bcrypt.
rounds = data.split('$')[2]
# work factor is logarithmic, adding one doubles the load.
diff = 2 ** (self.rounds - int(rounds)) - 1
while diff > 0:
self.encode(password, salt.encode('ascii'))
diff -= 1
And AUTH_USER_MODEL like:
from django.contrib.auth.hashers import check_password
from django.db import models
class User(models.Model):
id = models.BigAutoField(primary_key=True)
email = models.EmailField(unique=True)
password = models.CharField(max_length=120, blank=True, null=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
EMAIL_FIELD = 'email'
def check_password(self, raw_password):
def setter():
pass
alg_prefix = "bcrypt_php$"
password_with_alg_prefix = alg_prefix + self.password
return check_password(raw_password, password_with_alg_prefix, setter)
Settings base.py:
...
AUTH_USER_MODEL = 'custom.User'
PASSWORD_HASHERS = [
'custom.auth.hashers.BCryptPasswordHasher',
]
...
In that case, before the validation of password, I add bcrypt$ prefix and then do validation but in the database, the password is kept without bcrypt$.
It works but I'm wondering if there is some other easier way to do this, or maybe someone meets the same problem?
I want to add that both PHP application and new Django should support both formats and I cannot do changes on the legacy PHP. Changes only could be done on new Django server.
You can use your Custom authentication backend.
First create a file in your auth app (say you name it auth), call the file backends.py
Contents of backends.py
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import BaseBackend
UserModel = get_user_model()
class ModelBackend(BaseBackend):
"""
Authenticate against the settings ADMIN_LOGIN and ADMIN_PASSWORD.
Use the login name and a hash of the password. For example:
ADMIN_LOGIN = 'admin'
ADMIN_PASSWORD = 'pbkdf2_sha256$30000$Vo0VlMnkR4Bk$qEvtdyZRWTcOsCnI/oQ7fVOu1XAURIZYoOZ3iq8Dr4M='
"""
def authenticate(self, request, username=None, password=None, **kwargs):
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
if username is None or password is None:
return
try:
user = UserModel._default_manager.get_by_natural_key(username)
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
UserModel().set_password(password)
else:
if user.check_password(password):
# user exists
return user
Then in your settings.py include your backends.py in AUTHENTICATION_BACKENDS variables
It should look something like this
AUTHENTICATION_BACKENDS = [
"djangoprojectname.auth.backends.ModelBackend",
"django.contrib.auth.backends.ModelBackend",
]
Last point is to implement the check_password method in your auth model, based in our example here, we open file models.py in auth app, within the class User we add the method to implement check password based on bcrypt algorithm.
import bcrypt
class User(AbstractUser):
first_name = models.CharField(max_length=255, null=True)
email = models.CharField(unique=True, max_length=255, blank=True, null=True)
def check_password(self, raw_password):
def setter():
pass
check = bcrypt.checkpw(bytes(raw_password, 'utf-8'), bytes(self.password, 'utf-8'))
return check
def set_password(self, raw_password):
hashed = bcrypt.hashpw(bytes(raw_password, 'utf-8'), bcrypt.gensalt(rounds=10))
encrypted = str(hashed, 'UTF-8')
self.password = encrypted
self._password = encrypted
Then, in your login view probably you will have to implement something like this, in your views.py in auth app
username = data.get("username")
password = data.get("password")
if username is None or password is None:
pass # implement for requesting username and password
user = authenticate(username=username, password=password)
if user is None:
pass # implement for invalid credentials
# check user confirmation
confirmed = getattr(user, 'confirmed', None)
if confirmed is False or None:
pass # implement for user not confirmed
# check if user is active
isactive = getattr(user, 'isactive', None)
if isactive is False or None:
pass # implement for user account disabled
login(request, user)
# return view or json response or whatever for login success
To solve the inversed task of password checking for hashes generated with PHP's password_hash(password, PASSWORD_DEFAULT)
from django.contrib.auth.hashers import check_password
check_password(decoded_pass, 'bcrypt${0}'.format(php_hash), preferred='bcrypt')
worked for me.
Tested with Django 2.2. The bcrypt$ prefix hack is based on another SO answer.

I want to write a logic in views.py and save data in serializer

I am making a system saved user data to model.I want to write a part of logic in views.py and a part of save data in serializer.I want to make a system password is changed into hash.Now I wrote codes in views.py,
class InfoViews(viewsets.ModelViewSet):
queryset = Info.objects.all()
serializer_class = InfoSerializer
lookup_field = 'id'
def create(self,request, *args, **kwargs):
user = Info()
passwd = request.data['password']
md5 = hashlib.md5()
md5.update(passwd.encode('utf-8'))
user.password = md5.hexdigest()
user.save()
return JsonResponse({"data":"data"})
in serializer.py
class InfoSerializer(serializers.ModelSerializer):
created_time = serializers.DateTimeField(required=False)
class Meta:
model = Info
fields = '__all__'
def create(self, validated_data):
user = Info(
email=validated_data['email'],
username=validated_data['username'],
)
user.set_password(validated_data['password'])
user.save()
return user
in models.py
class Info(models.Model):
username = custom_fields.NotEmptyCharField(max_length=100, unique=True)
email = models.EmailField()
password = custom_fields.NotEmptyCharField(max_length=100)
class Meta:
db_table = 'info'
def __str__(self):
return '%s: %s' % (self.username, self.email)
Now whenI tried to save user data to model,django.core.exceptions.ValidationError: ['Can not be empty!'] error happens.What is wrong in my codes?I searched http://www.django-rest-framework.org/api-guide/serializers/ .How should I fix this?
You are not using InfoSerializer() serializer so, remove create() method from that, and change your views.py as below,
class InfoViews(ModelViewSet):
queryset = Info.objects.all()
serializer_class = InfoSerializer
lookup_field = 'id'
def create(self, request, *args, **kwargs):
serializer = InfoSerializer(request.data).data
serializer.pop('created_time', None)
passwd = serializer['password']
md5 = hashlib.md5()
md5.update(passwd.encode('utf-8'))
serializer['password'] = md5.hexdigest()
Info.objects.create(**serializer)
return JsonResponse({"data": "data"})
My Friendly Suggestion
I don't think this is a good method to acheive so, So changes below will do better (I think so ;))
views.py
class InfoViews(ModelViewSet):
queryset = Info.objects.all()
serializer_class = InfoSerializer
lookup_field = 'id'
serializer.py
import hashlib
class InfoSerializer(serializers.ModelSerializer):
created_time = serializers.DateTimeField(required=False)
def set_password(self, raw_pwd):
md5 = hashlib.md5()
md5.update(raw_pwd.encode('utf-8'))
return md5.hexdigest()
class Meta:
model = Info
fields = '__all__'
def create(self, validated_data):
validated_data['password'] = self.set_password(validated_data['password'])
return super().create(validated_data)
Update
Alternative create() for serializer,
def create(self, validated_data):
validated_data['password'] = self.set_password(validated_data['password'])
user = Info.objects.create(
email=validated_data['email'],
username=validated_data['username'],
password=validated_data['password']
)
# you can avoid large number of assignment statements (as above) by simply calling "super()" method
return user
You're getting a validation error because email is a required field. When you run user.save(), the email value isn't sent, hence the ValidationError.
You should definitely be saving everything in your view, the Serialiser is just a way to change the way the data is presented by DRF.
Also, you really shouldn't be using md5 to save your passwords. Just use the built in Django method: user.set_password(password) - Django will take care of the hashing for you and much more securely.

Update MongoDB ReferenceField using Flask-Admin

I'm trying to create an admin page from which I can edit what roles a user is member of using MonogDB and Flask-Admin.
models.py
class Role(db.Document, RoleMixin):
name = db.StringField(max_length=80, unique=True)
description = db.StringField(max_length=255)
def __unicode__(self):
return self.name
class User(db.Document, UserMixin):
email = db.StringField(max_length=255)
password = db.StringField(max_length=255)
roles = db.ListField(db.ReferenceField(Role))
admin.py
class UserView(ModelView):
from wtforms.fields import SelectMultipleField
from bson import ObjectId, DBRef
form_overrides = dict(roles=SelectMultipleField)
options = [(g.id, g.name) for g in models.Role.objects()]
# print options
# [(ObjectId('54a72849426c702850d01921'), u'community'),
# (ObjectId('54a72849426c702850d01922'), u'customer')]
form_args = dict(roles=dict(choices=options))
When I select a user role in the Flask-Admin edit_form view and cilck save, following form validation error is shown: '54a72849426c702850d01922' is not a valid choice for this field
What's the correct way to edit/update a ReferenceField ?
Your models look fine. But your ModelView is the problem. I'm using MongoEngine and here is my implementation for them.
class Role(db.Document, RoleMixin):
name = db.StringField(max_length=80, unique=True)
description = db.StringField(max_length=255)
def __unicode__(self):
return self.name
class User(db.Document, UserMixin):
email = db.StringField(max_length=255)
password = db.StringField(max_length=500)
active = db.BooleanField(default=True)
confirmed_at = db.DateTimeField()
roles = db.ListField(db.ReferenceField(Role), default=[])
# Optional to override save method.
def save(self, *args, **kwargs):
self.password = encrypt_password(self.password) # You can encrypt your password before storing in db, as a good practice.
self.confirmed_at = datetime.now()
super(User, self).save(*args, **kwargs)
Here are my model view:
class UserView(ModelView):
can_create = True
can_delete = True
can_edit = True
decorators = [login_required]
column_filters = ('email',)
def is_accessible(self):
return current_user.has_role("admin")
class RoleView(ModelView):
can_create = True
can_delete = True
can_edit = True
decorators = [login_required]
def is_accessible(self):
return current_user.has_role("admin")
You don't have to get all Roles objects explicitly, flask-admin would do it for you. You just have to create Roles first before creating User Object.
Also, you can create an initial user by using flask's before_first_request like this:
#app.before_first_request
def before_first_request():
user_datastore.find_or_create_role(name='admin', description='Administrator')
encrypted_password = encrypt_password('password') # Put in your password here
if not user_datastore.get_user('user#example.com'):
user_datastore.create_user(email='user#example.com', password=encrypted_password)
user_datastore.add_role_to_user('user#example.com', 'admin')
This would help you in updating references correctly.

Categories

Resources