Trying to add role-based folder permissions to the File DocType

I am trying to use ERPNext’s File module as a document management system, and it has most of the pieces I need, except for folder-level permissions.

Currently, there is no way to restrict specific users or roles to a particular folder—either they can see ALL non-private files or NONE of them.

To get around this, I created a new Doctype, Role_Folder_Test, which basically just uses two link fields to create an association between a role and a folder.

I then edited the has_permission block in Frappe/Core/Doctype/File/File.py to check if there is an association between that particular folder and a role that the current user has. If so, has_access = True. This check is used in addition to those existing in the original source code. Here is the section I modified:
2022-09-08 14_25_32-Window

And it ALMOST works, but not entirely: While it does affect a user’s ability to OPEN a file within a given folder, they are still able to SEE the folder itself and all of its contents; it only blocks you once you actually click a file.

That’s where I’m stuck—I looked around some other functions inside file.py, but nothing immediately stood out as controlling which files and folders are shown to the user.

Here is the actual code block:

def has_permission(doc, ptype=None, user=None):
	has_access = False
	user = (user or frappe.session.user)
	data = frappe.db.sql(
	"""
		select
			RFT.file
		from
			tabUser U
			inner join `tabHas Role` `HR` ON U.name = HR.parent
			inner join `tabRole_Folder_Test` `RFT` ON HR.role = RFT.role
		where 1=1
			AND HR.parentType = 'User'
			AND U.name = %(name)s
			AND RFT.file = %(docName)s
		""", {
			'name' : user,
			'docName': doc.name
			}
		)
	if ptype == "create":
			has_access = frappe.has_permission("File", "create", user=user)	
	if data:
		has_access = True;
	if doc.owner in [user, "Guest"] or user == "Administrator":
		has_access = True
	if doc.attached_to_doctype and doc.attached_to_name:
		attached_to_doctype = doc.attached_to_doctype
		attached_to_name = doc.attached_to_name
	try:
		ref_doc = frappe.get_doc(attached_to_doctype, attached_to_name)
		if ptype in ["write", "create", "delete"]:
			has_access = ref_doc.has_permission("write")
		if ptype == "delete" and not has_access:
			frappe.throw(
			("Cannot delete file as it belongs to {0} {1} for which you do not have permissions").format(doc.attached_to_doctype, doc.attached_to_name),
				frappe.PermissionError,)
		else:
			has_access = ref_doc.has_permission("read")
	except frappe.DoesNotExistError:
		# if parent doc is not created before file is created
		# we cannot check its permission so we will use file's permission
		pass
	return has_access

(Also, I’ve read a lot of threads about other people trying to make document management systems, and have seen several recommendations of using the NextCloud integration. I’m not opposed to that, if I can’t get this working, but I don’t need most enterprise DMS features, so I’d prefer to get it all working inside ERPNext unless it ends up being more manual work)

1 Like

Any progress on this?

Yep! Took me a minute because I’m new to python and kept screwing up my indentations, but I got something wired up!

Basically, in addition to file.py.has_permission (which covers whether you can actually open a file), there’s another function called “get_permission_query_conditions”, which covers what gets SHOWN to the user.

Technically, there wasn’t originally one in file.py; I had to register it in hooks.py first–there’s a list, called “permission_query_conditions”, where you can override the default permission queries on a per-doctype basis by listing
1: the doctype
and
2: the address of the file and function to use.

So I went to hooks.py, scrolled down to the permission_query_conditions list, and added the line
"File": "frappe.core.doctype.file.file.get_permission_query_conditions",
at the bottom.

Per the documentation, these permission queries should always return a text string containing a valid MariaDB WHERE clause.

Mine was basically the same as the query in my original has_permission query, except I had to tack on a UNION at the end, because at first it was only matching for files and ignoring folders.

Here’s the definition:

def get_permission_query_conditions(user):
    if not user:
        user = frappe.session.user
    return """
    (tabFile.owner in ({user}, "Guest")
    or tabFile._assign = {user}
    or {user} = "Administrator"
    or tabFile.name in
        (select
            T.name
        from
            tabUser U
            inner join tabHas Role HR ON U.name = HR.parent
            inner join tabRole_Folder_Test RFT ON HR.role = RFT.role
            inner join tabFile T ON RFT.file = T.name
        where 1=1
        AND HR.parentType = 'User'
            AND U.name = {user}
        UNION
        select
            T.name
        from
            tabUser U
            inner join tabHas Role HR ON U.name = HR.parent
            inner join tabRole_Folder_Test RFT ON HR.role = RFT.role
            inner join tabFile T ON RFT.file = T.folder
        where 1=1
            AND HR.parentType = 'User'
            AND U.name = {user}))""".format(user=frappe.db.escape(user))

See, the first union inner joins on tabFile.name, the second one joins on tabFile.folder.

You need both–otherwise, you’ll see ONLY folders or ONLY files.

2 Likes

thanks for the update
I will check

@brjohnson930 Could you please explain the doctype fields of Role_Folder_Test?

As you explained, one field will link to the Role and I am confused with the folder field.

Thanks!