Example #1
0
def is_listing_owner_or_admin(username, instance):
    profile = generic_model_access.get_profile(username)

    if profile.highest_role() not in ['APPS_MALL_STEWARD', 'ORG_STEWARD']:
        if profile not in instance.owners.all():
            raise errors.PermissionDenied(
                'User ({0!s}) is not an owner of this listing'.format(username))

    if instance.is_deleted:
        raise errors.PermissionDenied('Cannot update a previously deleted listing')

    return True
Example #2
0
def update_bookmark_entry_for_profile(request_profile, bookmark_entry_instance, bookmark_parent_object, title):
    """
    Update Bookmark Entries

    Method Responsibilities:
        * Rename folder bookmark titles
        * Moving folder/listing bookmarks under different folders

    Args:
        request_profile
        bookmark_entry_instance
        bookmark_parent_object:
            `BookmarkEntry` bookmark instance where request_profile want to put bookmark_entry_instance in that folder
        title
    """
    if bookmark_parent_object is None and title is None:
        raise errors.PermissionDenied('Need at least the bookmark_parent or title field')
    # Check to see if request profile has access to bookmark_entry_instance
    # (bookmark_entry_instance can be folder or listing bookmark)
    check_permission_for_bookmark_entry(request_profile, bookmark_entry_instance)

    folder_title_changed = False
    bookmark_entry_moved = False

    if bookmark_parent_object:
        if bookmark_parent_object.type != FOLDER_TYPE:
            raise errors.PermissionDenied('bookmark_parent_object needs to be a folder type')
        # make sure user has owner access on bookmark_parent_object
        check_permission_for_bookmark_entry(request_profile, bookmark_parent_object)

        # get bookmark entries to folder relationships for request_profile
        bookmark_entry_folder_relationships = bookmark_entry_instance.bookmark_parent.filter(
            bookmark_permission__profile=request_profile)
        bookmark_entry_instance.bookmark_parent.remove(*bookmark_entry_folder_relationships)

        bookmark_entry_instance.bookmark_parent.add(bookmark_parent_object)
        bookmark_entry_moved = True

    if bookmark_entry_instance.type == FOLDER_TYPE:
        if title:
            bookmark_entry_instance.title = title
            folder_title_changed = True

    bookmark_entry_instance.save()

    if bookmark_entry_moved or folder_title_changed:
        dispatcher.publish('update_bookmark_entry',
            bookmark_entry_instance=bookmark_entry_instance,
            bookmark_parent_object=bookmark_parent_object,
            folder_title_changed=folder_title_changed,
            bookmark_entry_moved=bookmark_entry_moved)

    return bookmark_entry_instance
Example #3
0
def share_bookmark_entry(request_profile, bookmark_entry_folder_to_share, target_profile, target_profile_bookmark_entry=None, target_user_type=None):
    """
    Add Profile Permission to Bookmark Entry

    Args:
        request_profile:
            Profile performing the action (view, add, edit, delete)
        bookmark_entry_folder_to_share:
            The folder `request_profile` is trying to share/add permission
        target_profile:
            The profile the `bookmark_entry_folder_to_share` should go to
        target_bookmark:

        target_user_type:

    Steps:
        check to see if `request_profile` has owner permission on the folder `bookmark_entry_folder_to_share` trying to be shared

    Defaults the target_user_type to BookmarkPermission.VIEWER
    """
    check_permission_for_bookmark_entry(request_profile, bookmark_entry_folder_to_share)
    target_user_type = target_user_type if target_user_type else models.BookmarkPermission.VIEWER

    if bookmark_entry_folder_to_share.type != FOLDER_TYPE:
        raise errors.PermissionDenied('bookmark_entry needs to be a folder type')

    # Check if target profile already has permissions for BookmarkEntry
    existing_target_profile_permission = models.BookmarkPermission.objects.filter(
        bookmark=bookmark_entry_folder_to_share,
        profile=target_profile
    ).first()

    if existing_target_profile_permission:
        raise errors.PermissionDenied('target user already has access to folder')

    # Add Bookmark entry to bookmark_entry_folder_to_share folder, add behaves like a set
    # target_profile_bookmark_entry.bookmark_parent.add(bookmark_entry_folder_to_share)
    if not target_profile_bookmark_entry:
        target_profile_bookmark_entry = create_get_user_root_bookmark_folder(target_profile)

    bookmark_entry_folder_to_share.bookmark_parent.add(target_profile_bookmark_entry)

    if bookmark_entry_folder_to_share.type == FOLDER_TYPE:
        bookmark_permission_folder = models.BookmarkPermission()
        bookmark_permission_folder.bookmark = bookmark_entry_folder_to_share
        bookmark_permission_folder.profile = target_profile
        bookmark_permission_folder.user_type = target_user_type
        bookmark_permission_folder.save()
        return bookmark_permission_folder
    elif target_bookmark.type == LISTING_TYPE:
        # LISTINGs inherit folder permissions
        pass
Example #4
0
def delete_listing(username, listing, delete_description):
    """
    TODO: need a way to keep track of this listing as being deleted.

    for now just remove
    """
    profile = generic_model_access.get_profile(username)
    # app_owners = [i.user.username for i in listing.owners.all()]
    # ensure user is the author of this review, or that user is an org
    # steward or apps mall steward

    # Don't allow 2nd-party user to be an delete a listing
    if system_anonymize_identifiable_data(profile.user.username):
        raise errors.PermissionDenied(
            'Current profile does not have delete permissions')

    priv_roles = ['APPS_MALL_STEWARD', 'ORG_STEWARD']
    if profile.highest_role(
    ) in priv_roles or listing.approval_status == 'IN_PROGRESS':
        pass
    else:
        raise errors.PermissionDenied(
            'Only Org Stewards and admins can delete listings')

    if listing.is_deleted:
        raise errors.PermissionDenied('The listing has already been deleted')

    old_approval_status = listing.approval_status
    listing = _add_listing_activity(profile,
                                    listing,
                                    models.ListingActivity.DELETED,
                                    description=delete_description)
    listing.is_deleted = True
    listing.is_enabled = False
    listing.is_featured = False
    listing.approval_status = models.Listing.DELETED
    # TODO Delete the values of other field
    # Keep lisiting as shell listing for history
    listing.save()
    # listing.delete()

    dispatcher.publish('listing_approval_status_changed',
                       listing=listing,
                       profile=profile,
                       old_approval_status=old_approval_status,
                       new_approval_status=listing.approval_status)
    def check_local_permission(self, entity):
        if self.sender_profile.highest_role() in ['APPS_MALL_STEWARD', 'ORG_STEWARD']:
            return True

        if self.sender_profile not in self.entity_dict['listing'].owners.all():
            raise errors.PermissionDenied('Cannot create a notification for a listing you do not own')
        else:
            return True
        return False
Example #6
0
    def destroy(self, request, pk=None):
        current_request_profile = model_access.get_self(request.user.username)

        if not current_request_profile.is_steward():
            raise errors.PermissionDenied('Only Stewards can delete notifications')

        queryset = self.get_queryset()
        notification_instance = get_object_or_404(queryset, pk=pk)
        notification_instance.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
Example #7
0
def update_profile_permission_for_bookmark_entry(request_profile, bookmark_permission_entry, user_type):
    """
    Update Profile Permission for Bookmark Entry

    Assumes all the permission checks happen before this is called for bookmark_permission_entry
    """
    bookmark_permission_entry_profile = bookmark_permission_entry.profile

    if request_profile == bookmark_permission_entry_profile:
        raise errors.PermissionDenied('can only update permissions for other users')

    bookmark_permission_entry.user_type = user_type
    bookmark_permission_entry.save()
    return bookmark_permission_entry
Example #8
0
def user_create_listing_condition(profile_obj, listing):
    """
    Listing create condition for user

    Args:
        profile_obj (models.Profile): Profile
        listing (models.Listing): Listing

    Return:
        bool: if user can create listings
    """
    if profile_obj not in listing.owners.all():
        raise errors.PermissionDenied(
            'Cannot create a notification for a listing you do not own')
    return True
Example #9
0
def get_bookmark_entry_by_id(request_profile, id):
    """
    Get bookmark entry by id and filter based on permissions
    Only owner or viewer of folder should be able to see it
    """
    # Validate to make sure user can see folder - bookmark_permission__profile
    query = models.BookmarkEntry.objects.filter(
        id=id,
        bookmark_parent__bookmark_permission__profile=request_profile
    ).first()

    if query is None:
        raise errors.PermissionDenied('Can not view bookmarks')

    return query
Example #10
0
def check_permission_for_bookmark_entry(request_profile, bookmark_entry, allow_owner=True, allow_viewer=False):
    """
    Check Permission for bookmark entry

    OWNER can view/add/edit/delete BookmarkEntry and BookmarkPermission models
    VIEWERs have no permissions
    """
    bookmark_entry_type = bookmark_entry.type

    profile_bookmark_permission = None

    if bookmark_entry_type == FOLDER_TYPE:
        profile_bookmark_permission = models.BookmarkPermission.objects.filter(
            profile=request_profile,
            bookmark=bookmark_entry)
    elif bookmark_entry_type == LISTING_TYPE:
        profile_bookmark_permission = models.BookmarkPermission.objects.filter(
            profile=request_profile,
            bookmark__bookmarkentry=bookmark_entry)

    # Convert query to BookmarkPermission object
    if profile_bookmark_permission:
        profile_bookmark_permission = profile_bookmark_permission.first()

    if not profile_bookmark_permission:
        raise errors.PermissionDenied('request profile does not have permission to view permissions')
    elif profile_bookmark_permission.user_type == models.BookmarkPermission.VIEWER:
        if allow_viewer is False:
            raise errors.PermissionDenied('request profile does not have permission to view permissions because of user_type')
    elif profile_bookmark_permission.user_type == models.BookmarkPermission.OWNER:
        if allow_owner is False:
            raise errors.PermissionDenied('request profile does not have permission to view permissions because of user_type')
    else:
        raise errors.PermissionDenied('request profile does not have permission')

    return profile_bookmark_permission
Example #11
0
    def destroy(self, request, pk=None):
        queryset = self.get_queryset()
        image = get_object_or_404(queryset, pk=pk)

        # TODO: Verify that only stewards can delete images and upload user

        # enforce access control
        if not system_has_access_control(self.request.user.username,
                                         image.security_marking):
            raise errors.PermissionDenied(
                'Security marking too high for current user')

        image.delete()
        # TODO: remove image from storage
        return Response(status=status.HTTP_204_NO_CONTENT)
Example #12
0
def edit_listing_review(username, review, rate, text=None):
    """
    Edit an existing review

    Args:
        username: user making this request
        review (models.Review): review to modify
        rate (int): rating (1-5)
        text (Optional(str)): review text

    Returns:
        The modified review
    """
    # only the author of a review can edit it
    user = generic_model_access.get_profile(username)
    if review.author.user.username != username:
        raise errors.PermissionDenied()

    change_details = [{
        'field_name': 'rate',
        'old_value': review.rate,
        'new_value': rate
    }, {
        'field_name': 'text',
        'old_value': review.text,
        'new_value': text
    }]

    listing = review.listing
    listing = _add_listing_activity(user,
                                    listing,
                                    models.ListingActivity.REVIEW_EDITED,
                                    change_details=change_details)

    review.rate = rate
    review.text = text
    review.edited_date = utils.get_now_utc()
    review.save()

    _update_rating(username, listing)

    dispatcher.publish('listing_review_changed',
                       listing=listing,
                       profile=user,
                       rating=rate,
                       text=text)
    return review
Example #13
0
def get_user_permissions_for_bookmark_entry(request_profile, bookmark_entry):
    """
    Get permissions for bookmark_entry

    Access Control
        Only Owners should be able to view all the permissions of a bookmark_entry
        Only Owners should be able to edit permissions of a bookmark_entry

    Access Control handle by get_bookmark_entry_by_id
    """
    check_permission_for_bookmark_entry(request_profile, bookmark_entry)

    if bookmark_entry.type != FOLDER_TYPE:
        raise errors.PermissionDenied('Can only check permissions for folder bookmarks')

    query = models.BookmarkPermission.objects.filter(bookmark=bookmark_entry)
    return query
Example #14
0
def _validate_create_bookmark_entry(request_profile=None, entry_type=None, folder_title=None, listing=None, is_root=None):
    """
    Validate values for creating bookmark entries
    """
    is_folder_type = (entry_type == FOLDER_TYPE)
    is_listing_type = (entry_type == LISTING_TYPE)
    is_root = is_root if is_root is True else False
    try:
        assert request_profile is not None, 'To create bookmark entry, profile is required'
        assert entry_type in BOOKMARK_TYPE_CHOICES, 'Entry Type needs to be one of the following: {}'.format(BOOKMARK_TYPE_CHOICES)

        if is_folder_type:
            if is_root is False and folder_title is None:
                raise AssertionError('Bookmark {} Entry require folder_title and is_root kwargs'.format(FOLDER_TYPE))
        elif is_listing_type and listing is None:
            raise AssertionError('Bookmark {} Entry require listing object'.format(LISTING_TYPE))
    except AssertionError as err:
        raise errors.PermissionDenied(err)
Example #15
0
def delete_listing_review(username, review):
    """
    Delete an existing review

    Args:
        username: user making this request
        review (models.Review): review to delete

    Returns:
        Listing associated with this review
    """
    profile = generic_model_access.get_profile(username)
    # ensure user is the author of this review, or that user is an org
    # steward or apps mall steward
    priv_roles = ['APPS_MALL_STEWARD', 'ORG_STEWARD']

    if profile.highest_role() in priv_roles:
        pass
    elif review.author.user.username != username:
        raise errors.PermissionDenied('Cannot update another user\'s review')

    # make a note of the change
    change_details = [
        {
            'field_name': 'rate',
            'old_value': review.rate,
            'new_value': None
        },
        {
            'field_name': 'text',
            'old_value': review.text,
            'new_value': None
        }
    ]
    # add this action to the log
    listing = review.listing
    listing = _add_listing_activity(profile, listing,
        models.ListingActivity.REVIEW_DELETED, change_details=change_details)

    # delete the review
    review.delete()
    # update this listing's rating
    _update_rating(username, listing)
    return listing
Example #16
0
def create_folder_bookmark_for_profile(request_profile, folder_name, bookmark_entry_folder=None, bookmark_children=None, listing_object=None):
    """
    Create Folder Bookmark for profile

    Args:
        profile (models.Profile): Profile
        folder_name (String): Folder name
        bookmark_entry_folder: (models.BookmarkEntry): Entry folder
        bookmark_children: (List of Integers)
    """
    bookmark_entry_folder = bookmark_entry_folder if bookmark_entry_folder else create_get_user_root_bookmark_folder(request_profile)
    bookmark_children = bookmark_children if bookmark_children else []

    if bookmark_entry_folder.type != FOLDER_TYPE:
        raise errors.PermissionDenied('bookmark_entry needs to be a folder type')

    # Only owners of bookmark_entry_folder should be able to add to folder
    check_permission_for_bookmark_entry(request_profile, bookmark_entry_folder)

    bookmark_folder_entry = create_bookmark_entry(request_profile, FOLDER_TYPE, folder_title=folder_name, is_root=False)

    # Add Permission so that user can see folder and make them owner
    add_profile_permission_for_bookmark_entry(
        request_profile,
        bookmark_entry_folder,
        request_profile,
        bookmark_folder_entry,
        target_user_type=models.BookmarkPermission.OWNER
    )

    # TODO: What is this doing?
    for bookmark_child in bookmark_children:
        update_bookmark_entry_for_profile(
            request_profile,
            get_bookmark_entry_by_id(request_profile, bookmark_child),
            bookmark_folder_entry,
            None)

    if listing_object:
        create_listing_bookmark_for_profile(request_profile, listing_object, bookmark_folder_entry)

    return bookmark_folder_entry
Example #17
0
    def retrieve(self, request, pk=None):
        """
        Return an image, enforcing access control
        """
        queryset = self.get_queryset()
        image = get_object_or_404(queryset, pk=pk)
        image_path = str(image.id) + '_' + image.image_type.name + '.' + image.file_extension
        # enforce access control
        profile = generic_model_access.get_profile(self.request.user.username)

        if not system_has_access_control(profile.user.username, image.security_marking):
            raise errors.PermissionDenied()

        content_type = 'image/' + image.file_extension
        try:
            with media_storage.open(image_path) as f:
                return HttpResponse(f.read(), content_type=content_type)
        except IOError:
            logger.error('No image found for pk {}'.format(pk))
            return Response(status=status.HTTP_404_NOT_FOUND)
def check_notification_permission(profile_instance, action, notification_type):
    """
    Check to see if user has permission

    Args:
        profile_instance(Profile): Profile Instance
        action(string): add/change/delete
        notification_type(string): notification type
    Return:
        True or PermissionDenied Exception
    """
    profile_role = profile_instance.highest_role()
    assert (profile_role in permission_dict), 'Profile group {} not found in permissions'.format(profile_role)

    user_action = '{}_{}_notification'.format(action, notification_type)

    profile_permission_list = permission_dict[profile_role]

    if user_action not in profile_permission_list:
        raise errors.PermissionDenied('Profile does not have [{}] permissions'.format(user_action))

    return True
Example #19
0
    def destroy(self, request, bookmark_pk=None, pk=None):
        """
        Validate make sure user has access

        if shared folder:
            if request_profile is viewer:
                raise PermissionDenied
            elif request_profile is owner and there is only one owner:
                Give owner "Are you sure you want to remove folder, it will affect all users"
            elif request_profile is owner and there is more than 1 owner:
                Remove Bookmark from user's bookmark list.
        else:
            Remove Bookmark from user's bookmark list.

        """
        request_profile = request.user.profile

        bookmark_entry = model_access.get_bookmark_entry_by_id(
            request_profile, bookmark_pk)
        bookmark_permission_entry = model_access.get_bookmark_permission_by_id(
            request_profile, bookmark_entry, pk)

        # TODO: refactor to remove_profile_permission_for_bookmark_entry
        bookmark_permission_entry_profile = bookmark_permission_entry.profile

        if request_profile == bookmark_permission_entry_profile:
            raise errors.PermissionDenied(
                'can only delete permissions for other users')

        # get bookmark entries to folder relationships for request_profile
        bookmark_entry_folder_relationships = bookmark_entry.bookmark_parent.filter(
            bookmark_permission__profile=bookmark_permission_entry_profile)

        bookmark_entry.bookmark_parent.remove(
            *bookmark_entry_folder_relationships)

        bookmark_permission_entry.delete()

        return Response(status=status.HTTP_204_NO_CONTENT)
Example #20
0
def create_listing_bookmark_for_profile(request_profile, listing, bookmark_entry_folder=None):
    """
    Create Listing Bookmark for profile
    """
    bookmark_entry_folder = bookmark_entry_folder if bookmark_entry_folder else create_get_user_root_bookmark_folder(request_profile)

    if bookmark_entry_folder.type != FOLDER_TYPE:
        raise errors.PermissionDenied('bookmark_entry needs to be a folder type')

    # Only owners of bookmark_entry_folder should be able to add to folder
    check_permission_for_bookmark_entry(request_profile, bookmark_entry_folder)

    bookmark_entry = create_bookmark_entry(request_profile, LISTING_TYPE, listing=listing)

    # Add Permission so that user can see folder and make them owner
    add_profile_permission_for_bookmark_entry(
        request_profile,
        bookmark_entry_folder,
        request_profile,
        bookmark_entry,
        target_user_type=models.BookmarkPermission.OWNER
    )

    return bookmark_entry
Example #21
0
def org_create_listing_condition(profile_obj, listing):
    if profile_obj not in listing.owners.all():
        raise errors.PermissionDenied(
            'Cannot create a notification for a listing you do not own')
    return True
Example #22
0
def _check_profile_permission(user_role_type, notification_action, notification_type, **kwargs):
    """
    Check Permission

    Permissions:
        APP_MALL_STEWARD
            can [CREATE, UPDATE, DISMISS, DELETE]
            notification type [SYSTEM, AGENCY, LISTING]

        ORG_STEWARD
            can [CREATE, UPDATE, DISMISS]
            notification type [AGENCY(org_steward_agency_condition), LISTING(c2)]
        USER
            can [CREATE, DISMISS]
            notification type [LISTING(c3)]

    Return:
        lambda function
    """
    profile_obj = kwargs.get('profile_obj')
    listing = kwargs.get('listing')

    permissions = {
        UserRoleType.APPS_MALL_STEWARD: {
            NotificationActionEnum.CREATE: {
                NotificationTypeEnum.SYSTEM: lambda: True,
                NotificationTypeEnum.AGENCY: lambda: True,
                NotificationTypeEnum.LISTING: lambda: True,
                NotificationTypeEnum.PEER: lambda: True
            },
            NotificationActionEnum.UPDATE: {
                NotificationTypeEnum.SYSTEM: lambda: True,
                NotificationTypeEnum.AGENCY: lambda: True,
                NotificationTypeEnum.LISTING: lambda: True,
                NotificationTypeEnum.PEER: lambda: True
            },
            NotificationActionEnum.DELETE: {
                NotificationTypeEnum.SYSTEM: lambda: True,
                NotificationTypeEnum.AGENCY: lambda: True,
                NotificationTypeEnum.LISTING: lambda: True,
                NotificationTypeEnum.PEER: lambda: True
            }
        },
        UserRoleType.ORG_STEWARD: {
            NotificationActionEnum.CREATE: {
                NotificationTypeEnum.SYSTEM: lambda: raise_(errors.PermissionDenied('Only app mall stewards can create system notifications')),
                NotificationTypeEnum.AGENCY: lambda: True,
                NotificationTypeEnum.LISTING: lambda: True,  # TODO: org_create_listing_condition(profile_obj, listing)
                NotificationTypeEnum.PEER: lambda: True
            },
            NotificationActionEnum.UPDATE: {
                # lambda: raise_(errors.PermissionDenied('Only app mall
                # stewards can update system notifications')),
                NotificationTypeEnum.SYSTEM: lambda: True,
                NotificationTypeEnum.AGENCY: lambda: True,
                NotificationTypeEnum.LISTING: lambda: True,
                NotificationTypeEnum.PEER: lambda: True
            },
            NotificationActionEnum.DELETE: {
                # lambda: raise_(errors.PermissionDenied('Only app mall
                # stewards can delete system notifications')),
                NotificationTypeEnum.SYSTEM: lambda: True,
                NotificationTypeEnum.AGENCY: lambda: True,
                NotificationTypeEnum.LISTING: lambda: True,
                NotificationTypeEnum.PEER: lambda: True
            }
        },
        UserRoleType.USER: {
            NotificationActionEnum.CREATE: {
                NotificationTypeEnum.SYSTEM: lambda: raise_(errors.PermissionDenied('Only app mall stewards can create system notifications')),
                NotificationTypeEnum.AGENCY: lambda: raise_(errors.PermissionDenied('Only org stewards can create agency notifications')),
                NotificationTypeEnum.LISTING: lambda: user_create_listing_condition(profile_obj, listing),
                NotificationTypeEnum.PEER: lambda: True
            },
            NotificationActionEnum.UPDATE: {
                NotificationTypeEnum.SYSTEM: lambda: raise_(errors.PermissionDenied('Only app mall stewards can update system notifications')),
                NotificationTypeEnum.AGENCY: lambda: raise_(errors.PermissionDenied('Only org stewards can create agency notifications')),
                NotificationTypeEnum.LISTING: None,
                NotificationTypeEnum.PEER: lambda: True
            },
            NotificationActionEnum.DELETE: {
                NotificationTypeEnum.SYSTEM: lambda: raise_(errors.PermissionDenied('Only app mall stewards can delete system notifications')),
                NotificationTypeEnum.AGENCY: lambda: raise_(errors.PermissionDenied('Only org stewards can create agency notifications')),
                NotificationTypeEnum.LISTING: None,
                NotificationTypeEnum.PEER: lambda: True
            }
        }
    }
    return permissions.get(user_role_type, {}).get(notification_action, {}).get(notification_type, lambda: raise_(errors.PermissionDenied('Unknown Permissions')))
Example #23
0
    def update(self, instance, validated_data):
        # logger.debug('inside ListingSerializer.update', extra={'request':self.context.get('request')})
        user = generic_model_access.get_profile(
            self.context['request'].user.username)

        if user.highest_role() not in ['APPS_MALL_STEWARD', 'ORG_STEWARD']:
            if user not in instance.owners.all():
                raise errors.PermissionDenied(
                    'User ({0!s}) is not an owner of this listing'.format(
                        user.username))

        if instance.is_deleted:
            raise errors.PermissionDenied(
                'Cannot update a previously deleted listing')

        change_details = []

        simple_fields = [
            'title', 'description', 'description_short', 'launch_url',
            'version_name', 'requirements', 'unique_name', 'what_is_new',
            'security_marking'
        ]

        for i in simple_fields:
            if getattr(instance, i) != validated_data[i]:
                change_details.append({
                    'old_value': getattr(instance, i),
                    'new_value': validated_data[i],
                    'field_name': i
                })
                setattr(instance, i, validated_data[i])

        if validated_data['is_enabled'] != instance.is_enabled:
            if validated_data['is_enabled']:
                model_access.enable_listing(user, instance)
            else:
                model_access.disable_listing(user, instance)

            instance.is_enabled = validated_data['is_enabled']

        if validated_data['is_private'] != instance.is_private:
            change_details.append({
                'old_value':
                model_access.bool_to_string(instance.is_private),
                'new_value':
                model_access.bool_to_string(validated_data['is_private']),
                'field_name':
                'is_private'
            })
            instance.is_private = validated_data['is_private']

        if validated_data['is_featured'] != instance.is_featured:
            if user.highest_role() not in ['APPS_MALL_STEWARD', 'ORG_STEWARD']:
                raise errors.PermissionDenied(
                    'Only stewards can change is_featured setting of a listing'
                )
            change_details.append({
                'old_value':
                model_access.bool_to_string(instance.is_featured),
                'new_value':
                model_access.bool_to_string(validated_data['is_featured']),
                'field_name':
                'is_featured'
            })
            instance.is_featured = validated_data['is_featured']

        s = validated_data['approval_status']
        if s and s != instance.approval_status:
            if s == models.Listing.APPROVED and user.highest_role(
            ) != 'APPS_MALL_STEWARD':
                raise errors.PermissionDenied(
                    'Only an APPS_MALL_STEWARD can mark a listing as APPROVED')
            if s == models.Listing.APPROVED_ORG and user.highest_role(
            ) not in ['APPS_MALL_STEWARD', 'ORG_STEWARD']:
                raise errors.PermissionDenied(
                    'Only stewards can mark a listing as APPROVED_ORG')
            if s == models.Listing.PENDING:
                model_access.submit_listing(user, instance)
            if s == models.Listing.APPROVED_ORG:
                model_access.approve_listing_by_org_steward(user, instance)
            if s == models.Listing.APPROVED:
                model_access.approve_listing(user, instance)
            if s == models.Listing.REJECTED:
                # TODO: need to get the rejection text from somewhere
                model_access.reject_listing(user, instance,
                                            'TODO: rejection reason')

        if instance.listing_type != validated_data['listing_type']:
            if instance.listing_type:
                old_value = instance.listing_type.title
            else:
                old_value = None
            if validated_data['listing_type']:
                new_value = validated_data['listing_type'].title
            else:
                new_value = None
            change_details.append({
                'old_value': old_value,
                'new_value': new_value,
                'field_name': 'listing_type'
            })
            instance.listing_type = validated_data['listing_type']

        image_keys = [
            'small_icon', 'large_icon', 'banner_icon', 'large_banner_icon'
        ]
        for image_key in image_keys:
            if validated_data[image_key]:
                old_value = model_access.image_to_string(
                    getattr(instance, image_key), True,
                    'old_value({0!s})'.format(image_key))
                new_value = model_access.image_to_string(
                    validated_data[image_key], False,
                    'new_value({0!s})'.format(image_key))

                if old_value != new_value:
                    new_value_image = None

                    old_image_id = None
                    if old_value is not None:
                        old_image_id = getattr(instance, image_key).id
                    if validated_data[image_key].get('id') == old_image_id:
                        new_value_image = getattr(instance, image_key)
                        new_value_image.security_marking = validated_data[
                            image_key].get('security_marking')
                        new_value_image.save()
                    else:
                        new_value_image = image_model_access.get_image_by_id(
                            validated_data[image_key].get('id'))

                        if new_value_image is None:
                            raise errors.InvalidInput(
                                'Error while saving, can not find image by id')

                    change_details.append({
                        'old_value': old_value,
                        'new_value': new_value,
                        'field_name': image_key
                    })

                    if image_key == 'small_icon':
                        instance.small_icon = new_value_image
                    elif image_key == 'large_icon':
                        instance.large_icon = new_value_image
                    elif image_key == 'banner_icon':
                        instance.banner_icon = new_value_image
                    elif image_key == 'large_banner_icon':
                        instance.large_banner_icon = new_value_image

        if 'contacts' in validated_data:
            old_contact_instances = instance.contacts.all()
            old_contacts = model_access.contacts_to_string(
                old_contact_instances, True)
            new_contacts = model_access.contacts_to_string(
                validated_data['contacts'])

            if old_contacts != new_contacts:
                change_details.append({
                    'old_value': old_contacts,
                    'new_value': new_contacts,
                    'field_name': 'contacts'
                })
                instance.contacts.clear()
                for contact in validated_data['contacts']:
                    # TODO: Smarter Handling of Duplicates Contact Records
                    # A contact with the same name and email should be the same contact
                    # in the backend.
                    # Person1(name='N1',email='*****@*****.**') and
                    #    Person1' (name='N1',email='*****@*****.**',secure_phone = '414-444-444')
                    # The two people above should be one contact
                    # if approval_status: "IN_PROGRESS" then it should be using
                    # contact model ids' since it is temporary contacts
                    obj, created = models.Contact.objects.get_or_create(
                        name=contact['name'],
                        email=contact['email'],
                        secure_phone=contact['secure_phone'],
                        unsecure_phone=contact['unsecure_phone'],
                        organization=contact.get('organization', None),
                        contact_type=contact_type_model_access.
                        get_contact_type_by_name(
                            contact['contact_type']['name']))
                    instance.contacts.add(obj)

        if 'categories' in validated_data:
            old_category_instances = instance.categories.all()
            old_categories = model_access.categories_to_string(
                old_category_instances, True)
            new_categories = model_access.categories_to_string(
                validated_data['categories'], True)
            if old_categories != new_categories:
                change_details.append({
                    'old_value': old_categories,
                    'new_value': new_categories,
                    'field_name': 'categories'
                })
                instance.categories.clear()
                for category in validated_data['categories']:
                    instance.categories.add(category)

        if 'owners' in validated_data:
            old_owner_instances = instance.owners.all()
            old_owners = model_access.owners_to_string(old_owner_instances,
                                                       True)
            new_owners = model_access.owners_to_string(
                validated_data['owners'], True)
            if old_owners != new_owners:
                change_details.append({
                    'old_value': old_owners,
                    'new_value': new_owners,
                    'field_name': 'owners'
                })
                instance.owners.clear()
                for owner in validated_data['owners']:
                    instance.owners.add(owner)

        # tags will be automatically created if necessary
        if 'tags' in validated_data:
            old_tag_instances = instance.tags.all()
            old_tags = model_access.tags_to_string(old_tag_instances, True)
            new_tags = model_access.tags_to_string(validated_data['tags'])
            if old_tags != new_tags:
                change_details.append({
                    'old_value': old_tags,
                    'new_value': new_tags,
                    'field_name': 'tags'
                })
                instance.tags.clear()
                for tag in validated_data['tags']:
                    obj, created = models.Tag.objects.get_or_create(
                        name=tag['name'])
                    instance.tags.add(obj)

        if 'intents' in validated_data:
            old_intent_instances = instance.intents.all()
            old_intents = model_access.intents_to_string(
                old_intent_instances, True)
            new_intents = model_access.intents_to_string(
                validated_data['intents'], True)
            if old_intents != new_intents:
                change_details.append({
                    'old_value': old_intents,
                    'new_value': new_intents,
                    'field_name': 'intents'
                })
                instance.intents.clear()
                for intent in validated_data['intents']:
                    instance.intents.add(intent)

        # doc_urls will be automatically created
        if 'doc_urls' in validated_data:
            old_doc_url_instances = model_access.get_doc_urls_for_listing(
                instance)
            old_doc_urls = model_access.doc_urls_to_string(
                old_doc_url_instances, True)
            new_doc_urls = model_access.doc_urls_to_string(
                validated_data['doc_urls'])
            if old_doc_urls != new_doc_urls:
                change_details.append({
                    'old_value': old_doc_urls,
                    'new_value': new_doc_urls,
                    'field_name': 'doc_urls'
                })

                new_doc_url_instances = []
                for d in validated_data['doc_urls']:
                    obj, created = models.DocUrl.objects.get_or_create(
                        name=d['name'], url=d['url'], listing=instance)
                    new_doc_url_instances.append(obj)
                for i in old_doc_url_instances:
                    if i not in new_doc_url_instances:
                        logger.info(
                            'Deleting doc_url: {0!s}'.format(i.id),
                            extra={'request': self.context.get('request')})
                        i.delete()

        # screenshots will be automatically created
        if 'screenshots' in validated_data:
            old_screenshot_instances = model_access.get_screenshots_for_listing(
                instance)
            old_screenshots = model_access.screenshots_to_string(
                old_screenshot_instances, True)
            new_screenshots = model_access.screenshots_to_string(
                validated_data['screenshots'])
            if old_screenshots != new_screenshots:
                change_details.append({
                    'old_value': old_screenshots,
                    'new_value': new_screenshots,
                    'field_name': 'screenshots'
                })

            new_screenshot_instances = []

            for s in validated_data['screenshots']:

                new_small_image = image_model_access.get_image_by_id(
                    s['small_image']['id'])
                new_small_image.security_marking = s['small_image'][
                    'security_marking']
                new_small_image.save()

                new_large_image = image_model_access.get_image_by_id(
                    s['large_image']['id'])
                new_large_image.security_marking = s['large_image'][
                    'security_marking']
                new_large_image.save()

                obj, created = models.Screenshot.objects.get_or_create(
                    small_image=new_small_image,
                    large_image=new_large_image,
                    listing=instance)

                new_screenshot_instances.append(obj)

            for i in old_screenshot_instances:
                if i not in new_screenshot_instances:
                    logger.info('Deleting screenshot: {0!s}'.format(i.id),
                                extra={'request': self.context.get('request')})
                    i.delete()

        if 'agency' in validated_data:
            if instance.agency != validated_data['agency']:
                change_details.append({
                    'old_value': instance.agency.title,
                    'new_value': validated_data['agency'].title,
                    'field_name': 'agency'
                })
                instance.agency = validated_data['agency']

        instance.save()

        # If the listing was modified add an entry showing changes
        if change_details:
            model_access.log_listing_modification(user, instance,
                                                  change_details)

        instance.edited_date = datetime.datetime.now(pytz.utc)
        return instance