Mongoengine object level security with Pyramid - acl

I've been trying to figure out how to do the object level security with mongoengine and haven't found any good solution.
Should I be using factories or something else? Somehow the getitem-function is never called, so I can't really do a thing in there. Also models ACL seems to be ignored completely. Is it possibility to validate the permissions of the user in model-level?
Case:
User is able to add a car.
User has a car and only he should be able to modify it.
config:
config = Configurator(settings=settings,
root_factory=RootFactory,
authentication_policy=AuthTktAuthenticationPolicy,
authorization_policy=ACLAuthorizationPolicy()
)
Model:
class Car(Document):
"""Car model"""
#property
def __acl__(self):
return [
(Allow, self.owner, 'modify'),
(Allow, 'group:admins', 'edit')
]
make = StringField(required=False)
model = StringField()
owner = ReferenceField('User')
Factories:
class RootFactory(object):
"""Root factory"""
def __init__(self, request):
self.__acl__ = [(Allow, Authenticated, 'create'),
(DENY_ALL)
]
class CarFactory(object):
__acl__ = [
(Allow, Authenticated, 'create'),
(Allow, 'self.owner', 'modify'),
(DENY_ALL)
]
def __init__(self, request):
self.request = request
def __getitem__(self, key):
user = USERS[key]
user.__parent__ = self
user.__name__ = key
return user
Routes:
config.add_route('saveCar', '/car/save/', factory=CarFactory)
View:
#view_config(route_name='saveCar',
request_method='POST',
permission='modify')
def saveCar(request):
"""Save car functions"""

Related

Update multiple model data through one serializer

Please go through the description, I tried to describe everything i've encountered while trying to solve this issue.
I have two models, User and DoctorProfile. User model has OneToOne relation with DoctorProfile. I'm trying to update data of both model through one serializer. I've combined two models into one serilzer like below:
class DoctorProfileFields(serializers.ModelSerializer):
"""this will be used as value of profile key in DoctorProfileSerializer"""
class Meta:
model = DoctorProfile
fields = ('doctor_type', 'title', 'date_of_birth', 'registration_number', 'gender', 'city', 'country', )
class DoctorProfileSerializer(serializers.ModelSerializer):
"""retrieve, update and delete profile"""
profile = DoctorProfileFields(source='*')
class Meta:
model = User
fields = ('name', 'avatar', 'profile', )
#transaction.atomic
def update(self, instance, validated_data):
ModelClass = self.Meta.model
profile = validated_data.pop('profile', {})
ModelClass.objects.filter(id=instance.id).update(**validated_data)
if profile:
DoctorProfile.objects.filter(owner=instance).update(**profile)
new_instance = ModelClass.objects.get(id = instance.id)
return new_instance
When I send request with GET method, the DoctorProfileSerializer returns nested data(Combining two models User and DoctorProfile) in the desired fashion.
But when I try to update both models through this serializer, it returns error saying User has no field named 'doctor_type'.
Let's have a look at the JSON i'm trying to send:
{
"name": "Dr. Strange updated twice",
"profile" : {
"doctor_type": "PSYCHIATRIST"
}
}
Let's have a look at how the serializer is receiving the JSON:
{
"name": "Maruf updated trice",
"doctor_type": "PSYCHIATRIST"
}
Models:
class CustomUser(AbstractBaseUser, PermissionsMixin):
class Types(models.TextChoices):
DOCTOR = "DOCTOR", "Doctor"
PATIENT = "PATIENT", "Patient"
#Type of user
type = models.CharField(_("Type"), max_length=50, choices=Types.choices, null=True, blank=False)
avatar = models.ImageField(upload_to="avatars/", null=True, blank=True)
email = models.EmailField(max_length=255, unique=True)
name = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
objects = CustomBaseUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['name', 'type'] #email is required by default
def get_full_name(self):
return self.name
def __str__(self):
return self.email
class DoctorProfile(models.Model):
"""Model for Doctors profile"""
class DoctorType(models.TextChoices):
"""Doctor will choose profession category from enum"""
PSYCHIATRIST = "PSYCHIATRIST", "Psychiatrist"
PSYCHOLOGIST = "PSYCHOLOGIST", "Psychologist"
DERMATOLOGIST = "DERMATOLOGIST", "Dermatologist"
SEXUAL_HEALTH = "SEXUAL HEALTH", "Sexual health"
GYNECOLOGIST = "GYNECOLOGIST", "Gynecologist"
INTERNAL_MEDICINE = "INTERNAL MEDICINE", "Internal medicine"
DEVELOPMENTAL_THERAPIST = "DEVELOPMENTAL THERAPIST", "Developmental therapist"
owner = models.OneToOneField(
CustomUser,
on_delete=models.CASCADE,
related_name='doctor_profile'
)
doctor_type = models.CharField(
_("Profession Type"),
max_length=70,
choices=DoctorType.choices,
null=True,
blank=False
)
title = models.IntegerField(_('Title'), default=1, choices=TITLES)
date_of_birth = models.DateField(null=True, blank=False)
gender = models.IntegerField(_('Gender'), default=1, choices=GENDERS)
registration_number = models.IntegerField(_('Registration Number'), null=True, blank=False)
city = models.CharField(_('City'), max_length=255, null=True, blank=True)
country = models.CharField(_('Country'), max_length=255, null=True, blank=True)
def __str__(self):
return f'profile-{self.id}-{self.title} {self.owner.get_full_name()}'
How do I know that the serializer is getting wrong JSON? I debugged the validated_data in the DoctorProfileSerializer and it's showing that it's a flat JSON, there's no key named profile.
I'm assuming the problem is with the source that I've added in the DoctorProfileSerializer. But if I don't use the source the get method returns the following error
Got AttributeError when attempting to get a value for field profile on serializer (DoctorProfileSerializer).
Please let me know if it's solvable also if it's a good approach to do it this way?
Ok, sorry if my answer is too long but let me try to answer step by step,
Models:
class DoctorProfile(models.Model):
# everything as it is
# except I feel comfortable using ForeignKey :D
owner = models.ForeignKey(
CustomUser,
on_delete=models.CASCADE,
related_name='doctor_profile'
)
# everything as it is
class CustomUser(AbstractBaseUser, PermissionsMixin):
# as it is
Serializers:
class DoctorProfileSerializer(serializers.ModelSerializer):
"""Serializer for DoctorProfile."""
class Meta(object):
model = DoctorProfile
fields = [
'id',
'doctor_type',
'title',
'date_of_birth',
'registration_number',
'gender',
'city',
'country',
]
read_only_fields = [
'id',
]
class CustomUserSerializer(serializers.ModelSerializer):
"""Serializer for DoctorProfile."""
# here I'm renaming the related object exactly as the
# related name you've provided on model
doctor_profile = DoctorProfileSerializer(many=False)
class Meta(object):
model = CustomUser
fields = [
'name',
'avatar',
'doctor_profile',
]
read_only_fields = [
'id',
]
def update(self, instance, validated_data):
# instance is the current row of CustomUser
# validated_data is the new incoming data
# use validated_data.pop('doctor_profile') to extract
# doctor_profile data and do whatever is needed on
# DoctorProfile model
# compare them and perform your update method
# as you wish on the DoctorProfile model
# object after updating models, you can query the total
# object again before returning if you want
return updated_object
View:
class CustomUserAPIView(RetrieveUpdateAPIView):
"""CustomUserAPIView."""
permission_classes = [IsAuthenticated]
model = CustomUser
serializer_class = CustomUserSerializer
lookup_field = 'id'
#for returning logged in user info only
def get_queryset(self):
return CustomUser.objects.filter(id=self.request.user.id).first()
def update(self, request, *args, **kwargs):
"""Update override."""
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(
instance,
data=request.data,
partial=partial,
)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
custom_user_obj = CustomUser.objects.filter(
id=instance.id,
).first()
serializer = CustomUserSerializer(custom_user_obj)
return Response(serializer.data)
Run the migration and let me know if you are getting the expected output on GET method. For UPDATE method if you face any problem let me know I will update the answer accordingly right away.
For keeping all the Django Rest Framework related docs handy, use this link
https://www.cdrf.co/

Adding Additional Data to a Serialize Response in Django

Updated
I changed my simplified question into a real example.
I've created a working post response of data from the model using ModelSerialzer, which I call from a post method in a view class. I would like to add additional data to the response. This is the pertinent code from my CBV:
def post(self, request, format=None):
user_profile = UserProfiles.objects.get(user=request.user.id)
service_id = user_profile.service_id
rec_filter = Recommendations.objects.values_list('resource')
if service_id > 0:
service_name = Services.objects.get(pk=service_id)
programs = Programs.objects.filter(services=service_id)
resources_filtered = Resources.objects.filter(program__in=programs).exclude(id__in=rec_filter)
else:
service_name = 'All Services'
resources_filtered = Resources.objects.exclude(id__in=rec_filter)
serializer = ResourceSerializer(resources_filtered, many=True)
#serializer.data["service"] = service_name
return Response(serializer.data)
The commented out line was my attempt to add data base on a similar post here. I get a 500 response in my API call. What is the correct way to do it? The response data is JSON if that's necessary to mention.
This is the ModelSerializer:
class ResourceSerializer(serializers.ModelSerializer):
organization = OrganizationSerializer(read_only=True)
program = ProgramSerializer(read_only=True)
class Meta:
model = Resources
fields = [
'organization',
'program',
'link',
'contact',
'general_contact',
'eligibility',
'service_detail'
]
Test of the answer
Heres the updated code based on the answer with a correction to fix and error:
class ResourceSerializer(serializers.ModelSerializer):
organization = OrganizationSerializer(read_only=True)
program = ProgramSerializer(read_only=True)
service = serializers.SerializerMethodField()
def get_service(self, obj):
return "All Services"
class Meta:
model = Resources
fields = [
'organization',
'program',
'link',
'contact',
'general_contact',
'eligibility',
'service_detail',
'service'
]
The problem with this approach is that the value "All Services" is repeated in every row serialized. It's only needed once. I'd also like to keep the data transmitted minimized.
The problem with the original attempt is that serializer.data is immutable. It's necessary to make a copy and add to it.
serializer = ResourceSerializer(resources_filtered, many=True)
augmented_serializer_data = list(serializer.data)
augmented_serializer_data.append({'service': 'All Services'})
return Response(augmented_serializer_data)
This answer is based on one given by #andre-machado in this question.
This code here is an example to coincide with the other answer given.
You can do it in serializer itself. Define the new field required and add it in fields. Mark all the fields in serializer from resource model.
class ResourceSerializer(serializers.ModelSerializer):
service = serializers.SerializerMethodField()
def get_service(self):
return "All Services"
class Meta :
model = Resources
fields = ('service') #Mark all the fields required here from resource model
You can do it from the serilaizer. In this case i was adding the field isOpen to the response and this is how i did it .timeDifference is the name of the function that was to generate data for the extra field . I hope it helps
class ShopSearchSerializer(serializers.ModelSerializer):
isOpen = serializers.SerializerMethodField('timeDifference')
def timeDifference(self,*args):
requestTime = datetime.now()
return requestTime
class Meta:
model = Shop
fields =['name','city','street','house','opening_time','closing_time','isOpen']

How to modify a many-to-many collection using django rest framework

I am trying to create an endpoint where, having a User entity, I can add / remove existing Group entities to user.groups many-to-many field. But when I try to do it, django-rest-framework tries to create new group objects instead of finding existing ones.
I have defined two serializers where UserSerializer has a nested GroupSerializer:
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
fields = ['id', 'name']
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'groups']
groups = GroupSerializer(many=True)
def update(self, instance, validated_data):
data = validated_data.copy()
groups = data.pop('groups', [])
for key, val in data.items():
setattr(instance, key, val)
instance.groups.clear()
for group in groups:
instance.groups.add(group)
return instance
def create(self, validated_data):
data = validated_data.copy()
groups = data.pop('groups', [])
instance = self.Meta.model.objects.create(**data)
for group in groups:
instance.groups.add(group)
return instance
When I send a JSON through a PUT REST call (from django-rest-framework web interface):
{
"id": 6,
"username": "user5#example.com",
"email": "user5#example.com",
"groups": [
{
"id": 1,
"name": "AAA"
}
]
}
I expect serializer to find the Group with given id and add it to User. But instead, it tries to create a new user group and fails with duplicate key error:
{
"groups": [
{
"name": [
"group with this name already exists."
]
}
]
}
I searched over the internet and debugged myself and found no solution to this use case.
The create and update methods inside UserSerializerclass are never reached.
Edit: as asked, here are my views and urls:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all().order_by('-date_joined')
serializer_class = UserSerializer
class GroupViewSet(viewsets.ModelViewSet):
queryset = Group.objects.all()
serializer_class = GroupSerializer
Urls:
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'groups', views.GroupViewSet)
urlpatterns = [
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
This seems to be the validation error due to a nested serializer model that contains unique constraint, see this post. According to the article, DRF did not handle this condition since it's hard to realize if the serializer is a nested serializer within another one. And that's why the create() and update() never been reached since the validation is done before calling them.
The way to work around this is to remove the uniqueness validator manually in GroupSerializer as follow:
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
fields = ['id', 'name']
extra_kwargs = {
'name': {'validators': []},
}
BTW, there are some points that can be improved or should be corrected in your update() and create() code. Firstly, you didn't do instance.save() so the instance won't be update after the whole process done. Second, the groups are just a list of dictionary, and you should not add object that way. The following are the modification based on your OP code:
def update(self, instance, validated_data):
data = validated_data.copy()
groups = data.pop('groups', [])
for key, val in data.items():
setattr(instance, key, val)
instance.save() # This will indeed update DB values
group_ids = [g['id'] for g in groups]
instance.groups.clear()
instance.groups.add(*group_ids) # Add all groups once. Also you can replace these two lines with
# instance.groups.set(group_ids)
return instance
Though am late, here is how I did it, adding to Tsang-Yi Shen answer. However this worked for me because I was using django-role-permissions https://django-role-permissions.readthedocs.io/en/stable/setup.html
For the group serializer am only interested in the name.
from rolepermissions.roles import assign_role
class GroupModelSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField()
class Meta:
model = Group
fields = ['name']
extra_kwargs = {
'name': {'validators': []},
}
def get_name(self, obj):
"""
This method modifies the way the name field is returned in a get request.
"""
return [group.name for group in obj.objects.all()]
In the UserSerializer, I modify the groups field to be returned as a list of groups a user belongs to, and received as a list instead of an OrderedDict in the JSON payload for creating a User, which can look something like this:
{
"email": "testuser#example.com",
"username": "testuser",
"name": "Test User",
"password": "password123",
"groups": ["doctor", "nurse", ]
}
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
groups = ListField(required=False, default=[], write_only=True)
user_groups = serializers.SerializerMethodField(read_only=True)
class Meta:
model = User
fields = ["id", "email", "username", "name", "password", "groups", "user_groups"]
depth = 2
def get_user_groups(self, obj):
"""
This method modifies the way the `groups` field is returned in a get request.
"""
return [group.name for group in obj.groups.all()]
def create(self, validated_data):
password = validated_data.pop('password')
gropus_to_add_user = validated_data.pop("groups")
user = User(**validated_data)
user.set_password(password)
user.save()
for group_name in groups_to_add_user:
# The assign_role function adds the user to a group.
# In this case group and roles may mean the same thing.
assign_role(user, group_name)
return user
You will have something like this when creating a user, here am using Swagger:

How to do an update method for a nested django rest framework APi Boolean ? [OnetoOneField]

So i have been researching about how to update the nested serializer with onetoonefield. However it has not been able to solve my problem. As i am still new to django rest framework, i am still inexperience about what is the problem as i never done an API before.
models.py
class Membership(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
membership = models.BooleanField(default=False)
serializers.py
class MembershipSerializer(serializers.ModelSerializer):
class Meta:
model = Membership
fields = ('membership',)
class UserSerializer(serializers.ModelSerializer):
membership = MembershipSerializer(many=False)
class Meta:
model = User
fields = ('id', 'username', 'email', 'password', 'first_name', 'last_name', 'is_staff', 'membership',)
read_only_fields = ('id',)
def create(self, validated_data):
membership_data = validated_data.pop('membership')
user = User.objects.create(**validated_data)
Membership.objects.create(user=user, **membership_data)
return user
def update(self, instance, validated_data):
instance.username = validated_data.get('username', instance.username)
instance.email = validated_data.get('email', instance.email)
instance.password = validated_data.get('password', instance.password)
instance.first_name = validated_data.get('first_name', instance.first_name)
instance.last_name = validated_data.get('last_name', instance.last_name)
instance.is_staff = validated_data.get('is_staff', instance.is_staff)
instance.save()
membership_data = validated_data.get('membership')
membership_id = membership_data.get('id', None)
if membership_id:
membership_item = Membership.objects.get(id=membership_id, membership=instance)
membership_item.membership = membership_data.get('membership', membership_item.name)
membership_item.user = membership_data.get('user', membership_item.user)
membership_item.save()
return instance
views.py
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all().order_by('-date_joined')
serializer_class = UserSerializer
permission_classes = [IsAuthenticated]
def get_permissions(self):
# allow non-authenticated user to create
return (AllowAny() if self.request.method == 'POST'
else permissions.IsStaffOrTargetUser()),
screenshot of api
https://i.imgur.com/dDqthRu.png
As you can see above, my membership is null, i have no idea why so i tested with is_staff to check and it is using false like a normal Booleanfield. This has make me wonder what is wrong with my models for the membership boolean field.
Main problem
As i am using a boolean field, i was trying to get the user membership to be updated. So i try to use PUT method and the result is nothing has change after i check the membership box and click on PUT.
And if i just want to update the username, i have to check on the membership box else it will give me this:
https://i.imgur.com/KpzHIsE.png
I have been checking online for several solution and none of them has work for me with the update method. I am also puzzle by the null value in the api for the booleanfield membership.

Plone/SQLAlchemy - How can I properly adapt an interface to a form for editing records in a backend database (using mapped class)?

I am trying to create a form for editing a record in a table.
Here is a brief sample of my Interface:
class ICalibration(Interface):
"""Interface class for Calibration
"""
Calibration_ID = schema.Int(title=u"Calibration_ID",
required=False
)
...
Calibration_Type = schema.Choice(title=u"Calibration Type",
description=u"Type of Calibration",
source=_calibrationTypes,
required=True,
)
...
Last_Calibration = schema.Datetime(title=u"Last Calibration",
required=False,
)
...
Here is a brief sample of the ORMBase class:
#implementer(ICalibration)
class Calibration(ORMBase)
__tablename__="Calibrations"
Calibration_ID = sqlalchemy.Column(sqlalchemy.Integer(),
primary_key=True,
autoincrement=True
)
Calibration_Type = sqlalchemy.Column(sqlalchemy.Integer(),
nullable=False
)
...
Last_Calibration = sqlalchemy.Column(sqlalchemy.types.DateTime(),
nullable=True,
)
...
Here is what I have so for the form class:
class EditCalibration(form.SchemaForm):
grok.name('edit-calibration')
grok.require('zope2.View')
grok.context(ISiteRoot)
schema = ICalibration
id = None
#memoize
def getContent(self):
self.id = self.request.get('id',None)
if self.id:
return session.query(Calibration).filter(Calibration.Calibration_ID == self.id).one()
#button.buttonAndHandler(u'Submit')
def handleOk(self, action):
data, errors = self.extractData()
#do something with data
....
#button.buttonAndHandler(u'Cancel')
def handleCancel(self, action):
#redirect user
....
The values themselves do show up properly in their respective fields, but I am getting the error in the debug menu:
TypeError: ('Could not adapt', None, <InterfaceClass gpcl.calibration.calibration.ICalibration>)
How can I fix this problem so that it adapts correctly?
Also, for reference, I actually asked a question before that got an answer providing code for the Calibration and CalibrationForm classes. Because the fields fill out fine, there is probably something I did wrong.