Alternate way do delete users

Continuing the discussion from Implementing a paid content subscription model on a Plone site:

I have found an issue in the site we implemented the subscription model:

currently the site has around 800 users that have been created using the following code:

def create_subscriber(self, code, email, group):
    api.user.create(email=email, username=email, properties=dict(code=code))
    api.user.revoke_roles(email, roles=['Member'])
    api.group.add_user(groupname=group, username=email)

the problem arises when I try to delete one of the users:

def remove_subscriber(self, email):
    with api.env.adopt_roles(['Manager']):
        api.user.delete(username=email)

as it takes a lot of memory and the instance is restarted by memmon.

is this normal, or is a bug?

checking the code in the membership tool, I see the following things are done:

  1. delete members in acl_users
  2. delete member data in portal_memberdata
  3. delete members' home folders including all content items
  4. delete members' local roles

one of this parts is consuming a lot of memory but I have no idea which one.

I think I don't need steps 3 and 4; may I write my own method and skip them to avoid issues with memory consumption?

def remove_subscriber(self, email):
    membership_tool = api.portal.get_tool('portal_membership')
    with api.env.adopt_roles(['Manager']):
        membership_tool.deleteMembers([email], delete_memberareas=False, delete_localroles=False):

Deleting the local roles is what takes up the memory: it goes through the whole site, waking up all objects, which can be a lot if you have a big site.

For one client where we ran into this, we solved it with the following patch.
Note that you may need to tweak the depth option to fit your site.
The numbers mentioned in the docstring are for one specific site.

from Acquisition import aq_base
from Products.CMFCore.utils import _checkPermission
from Products.CMFCore.MembershipTool import MembershipTool

def deleteLocalRoles(self, obj, member_ids, reindex=1, recursive=0,
                     REQUEST=None, depth=3):
    """Delete local roles of specified members.

    This takes far too much memory.  See if we can reduce this.

    We have tried convincing Zope to release memory by using
    savepoints (transaction.savepoint(optimistic=True)).  We tried
    using the catalog to search for only folderish items, and do
    explicit garbage collection.  Nothing helped.

    Now we add a depth on which we search.  This is a fluid depth.
    If the object does not need deletion of local roles and there
    are no interesting local roles at all, we decrease the depth,
    thus eliminating uninteresting folders where the member likely
    has no local roles anywhere.

    Yes, this may fail to delete some local roles.  But at least it
    usually finishes within a few seconds instead of about a minute.
    And it does not consume over 1.5 GB of memory.  So be happy.

    Note: with a depth of 4 you would already crawl about 90 percent
    of the site, so that would hardly help.
    """
    delete = False
    if _checkPermission(ChangeLocalRoles, obj):
        has_local_roles = False
        for user, roles in obj.get_local_roles():
            if user in member_ids:
                delete = True
            if len(roles) == 0:
                # I guess this cannot happen, but let's be safe.
                continue
            elif len(roles) > 1:
                has_local_roles = True
                break
            elif roles[0] == 'Owner':
                # Only one uninteresting role.
                continue
            else:
                has_local_roles = True
                break
        if delete:
            # At least one to-be-deleted role has been found.
            obj.manage_delLocalRoles(userids=member_ids)
        elif not has_local_roles:
            # Nothing deleted at this level, and no interesting local
            # roles for other users.  Decrease search depth.
            depth -= 1
            if depth <= 0:
                # Ignore the rest of this content tree, if any.
                return

    if recursive and hasattr(aq_base(obj), 'contentValues'):
        for subobj in obj.contentValues():
            self.deleteLocalRoles(subobj, member_ids, 0, 1, depth=depth)

    if reindex and hasattr(aq_base(obj), 'reindexObjectSecurity'):
        # reindexObjectSecurity is always recursive
        obj.reindexObjectSecurity()


def patch_membershiptool():
    MembershipTool._orig_deleteLocalRoles = MembershipTool.deleteLocalRoles
    MembershipTool.deleteLocalRoles = deleteLocalRoles


def unpatch_membershiptool():
    MembershipTool.deleteLocalRoles = MembershipTool._orig_deleteLocalRoles
4 Likes

thanks, I ended with this and it seems to be working now:

def remove_subscriber(self, email):
    """Remove subscriber. We can not use `api.user.delete` as, by
    default, it tries to remove member areas and local roles; the
    later consumes a lot of memory and is not necessary for us.
    """
    membership_tool = api.portal.get_tool('portal_membership')
    with api.env.adopt_roles(['Manager']):
        membership_tool.deleteMembers(
            [email], delete_memberareas=False, delete_localroles=False)
1 Like

Has anyone recently dealt with the situation of deleting a user from a content-heavy site? Deleting a user on sites like that takes a long time because of the removal of local roles. Any ideas? I saw this:

If local roles are not a thing in your site, or you are pretty sure that user ids will not clash in the future, you can skip the local role checking when deleting the users.

You can use the REST API to achieve that: Users — Plone Documentation v6

Or a custom script that calls deleteMembers with the proper parameters:

portal_membership.deleteMembers(
            (self._get_user_id,),
            delete_memberareas=False,
            delete_localroles=False,
        )

The problem with this is that deleted users continue to appear on the local permission assignment screen, which can be confusing.

Perhaps the assignment of local permissions should be indexed in some way?

the allowed roles and users Index reads the value from the localRole. Plone has no way to know the user has been deleted, think to external authentication for example. Users are checked at the time they try to do something. Showing the state depends on the security info in the object. The only way is to fix the permission on the object.

The only way I see it to never use users but groups. So, if you've UserA, create the group UserA with the UserA and use the group for permission instead of the user. So deletion is just about removing the user from the group and deleting the user.

Generally speaking, use groups and never assign role or permissions to the single user.

1 Like