Skip to content

Restricted access#756

Open
kernicPanel wants to merge 24 commits into
mainfrom
restricted-access
Open

Restricted access#756
kernicPanel wants to merge 24 commits into
mainfrom
restricted-access

Conversation

@kernicPanel

@kernicPanel kernicPanel commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Purpose

Allow marking a folder as "restricted": it stops inheriting roles and link
configuration from its ancestors, and only explicit accesses granted from
the folder down apply.

Proposal

  • Add is_restricted to Item (folders only), exposed in the API with a restrict ability for owners
  • Cut role and link reach inheritance at restriction boundaries everywhere: role resolution, user_roles annotation, nb_accesses, access management, link propagation, search, ZIP export, search index ACLs
  • Grant the activating user an explicit owner access on activation; clean up accesses and link reach made redundant on deactivation
  • Uproot (move to root, keeping accesses and content) restricted folders that would otherwise be trashed by users without access to them
  • Route every permission decision through a swappable backend (core/permissions, PERMISSIONS_BACKEND setting), one property per ability, paving the way for ABAC
  • Fix parent() to resolve the direct parent by exact path

@kernicPanel
kernicPanel force-pushed the restricted-access branch 7 times, most recently from e8d21cc to 001fd58 Compare July 6, 2026 13:24
kernicPanel added a commit that referenced this pull request Jul 6, 2026
The soft delete assertion sat inside the pytest.raises block and
never ran, hiding a wrong expected message. Indexer error tests
now keep a single raising invocation inside the block so the
failure source is unambiguous. Flagged by Sonar on PR #756.
kernicPanel added a commit that referenced this pull request Jul 6, 2026
The soft delete assertion sat inside the pytest.raises block and
never ran, hiding a wrong expected message. Indexer error tests
now keep a single raising invocation inside the block so the
failure source is unambiguous. Flagged by Sonar on PR #756.
kernicPanel added a commit that referenced this pull request Jul 6, 2026
The soft delete assertion sat inside the pytest.raises block and
never ran, hiding a wrong expected message. Indexer error tests
now keep a single raising invocation inside the block so the
failure source is unambiguous. Flagged by Sonar on PR #756.
@kernicPanel
kernicPanel requested review from NathanVss and lunika July 6, 2026 16:32
@kernicPanel
kernicPanel marked this pull request as ready for review July 6, 2026 16:33
kernicPanel added a commit that referenced this pull request Jul 6, 2026
The soft delete assertion sat inside the pytest.raises block and
never ran, hiding a wrong expected message. Indexer error tests
now keep a single raising invocation inside the block so the
failure source is unambiguous. Flagged by Sonar on PR #756.
kernicPanel added a commit that referenced this pull request Jul 7, 2026
The soft delete assertion sat inside the pytest.raises block and
never ran, hiding a wrong expected message. Indexer error tests
now keep a single raising invocation inside the block so the
failure source is unambiguous. Flagged by Sonar on PR #756.
@kernicPanel
kernicPanel force-pushed the restricted-access branch 2 times, most recently from ee6b94a to 8d0b54d Compare July 7, 2026 14:10
kernicPanel added a commit that referenced this pull request Jul 7, 2026
The soft delete assertion sat inside the pytest.raises block and
never ran, hiding a wrong expected message. Indexer error tests
now keep a single raising invocation inside the block so the
failure source is unambiguous. Flagged by Sonar on PR #756.
kernicPanel added a commit that referenced this pull request Jul 7, 2026
The soft delete assertion sat inside the pytest.raises block and
never ran, hiding a wrong expected message. Indexer error tests
now keep a single raising invocation inside the block so the
failure source is unambiguous. Flagged by Sonar on PR #756.
kernicPanel added a commit that referenced this pull request Jul 7, 2026
The soft delete assertion sat inside the pytest.raises block and
never ran, hiding a wrong expected message. Indexer error tests
now keep a single raising invocation inside the block so the
failure source is unambiguous. Flagged by Sonar on PR #756.
@kernicPanel
kernicPanel marked this pull request as draft July 7, 2026 16:32
Allow owners to activate/deactivate restriction on folders
via PATCH with permission check through get_abilities.
kernicPanel added a commit that referenced this pull request Jul 9, 2026
The soft delete assertion sat inside the pytest.raises block and
never ran, hiding a wrong expected message. Indexer error tests
now keep a single raising invocation inside the block so the
failure source is unambiguous. Flagged by Sonar on PR #756.
kernicPanel added a commit that referenced this pull request Jul 9, 2026
The soft delete assertion sat inside the pytest.raises block and
never ran, hiding a wrong expected message. Indexer error tests
now keep a single raising invocation inside the block so the
failure source is unambiguous. Flagged by Sonar on PR #756.
@kernicPanel
kernicPanel marked this pull request as ready for review July 9, 2026 10:32
Parent owner excluded from a restricted child folder cannot
trash it. The folder is moved to root intact with its accesses
and descendants preserved.
Roles granted above a restricted folder were leaking through the
annotation subquery used by children lists and search results,
and through the non-annotated get_role fallbacks of ItemAccess
and Invitation. The annotation is now built by the permissions
backend, dropping the duplicated ItemQuerySet override.
Restricted folders can set any link reach regardless of parent
configuration. Propagating link reach to descendants now stops
at restriction boundaries.
Restricted folders accept any explicit role regardless of ancestor
roles. Synchronizing accesses to descendants now stops at
restriction boundaries.
Items behind a restriction boundary with no effective role are
now filtered out after role annotation.
Items behind a restriction boundary are now filtered out of ZIP
exports using role annotation.
get_ancestor_to_descendants_map now queries for restricted folders
among ancestor paths and stops the mapping at restriction boundaries,
preventing inherited accesses from crossing into restricted subtrees.
TreeModel.parent() uses .last() which follows Meta.ordering
(created_at). After a move(), the grandparent created later
is returned instead of the direct parent. The parent path is
known statically, so look it up by equality instead of
scanning ancestors.
Sonar S3516 flags validate() because every return yields the
same attrs value. Merge the restricted early return into an
if/else with a single exit and move the ancestors validation
to a helper to keep cognitive complexity low; behavior is
unchanged.
The soft delete assertion sat inside the pytest.raises block and
never ran, hiding a wrong expected message. Indexer error tests
now keep a single raising invocation inside the block so the
failure source is unambiguous. Flagged by Sonar on PR #756.
Each ability now reads as a named rule on ItemAbilities, fixing
the Sonar S3776 complexity of the former monolithic function.
Per-action properties also sketch the vocabulary a future ABAC
engine will implement, one check per action.
Setting is_restricted required a create then update round trip, since
CreateItemSerializer didn't expose the field. The creator is always
granted owner access right after creation, so the restrict ability is
already satisfied at that point.

Restricting a file is rejected, mirroring the existing DB constraint
that only folders can be restricted.
@sonarqubecloud

sonarqubecloud Bot commented Jul 9, 2026

Copy link
Copy Markdown

)

if attrs.get("is_restricted") and attrs["type"] != models.ItemTypeChoices.FOLDER:
raise serializers.ValidationError(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this constraint should be defined on the model directly

Comment on lines +1033 to +1037
GistIndex(
fields=["path"],
name="drive_item_restricted_path_ix",
condition=models.Q(is_restricted=True),
),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "why" of this is not announced in the commit and no tests confirm the sought behavior.

# Pre-compute number of accesses
# Pre-compute number of accesses; the parent's count does not apply
# to restricted children which cut inheritance
item_nb_accesses = item.nb_accesses

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be renamed to nb_effective_accesses.
The current name is a bit confusing. I had to reread the nb_accesses method on the model to remember what past me wrote. 😅

Comment on lines +42 to +47
if item.is_restricted:
# Inheritance is cut on the item itself: only direct accesses apply
return models.ItemAccess.objects.filter(
Q(user=user) | Q(team__in=user.teams),
item=item,
).values_list("role", flat=True)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

smells a bit like AI slope.

Here you are shortcircuiting your own roles_at method instead of passing it an argument "should_inherit=True" and let it do the job.
And above _cut_by_restriction is worse because you went down to the path from the item and go back to check is_restricted on the item via a query 😲

I think you should trash the _cut_by_restriction method and modify each existing filter clause directly looking at the should_inherit argument.

Comment on lines +32 to +39
@abstractmethod
def ancestors_links_paths_mapping(self, item: models.Item) -> dict[str, list[dict]]:
"""Return the link definitions applying to each ancestor path of the item."""

@abstractmethod
def link_definition_for(self, item: models.Item) -> dict:
"""Return the effective link definition of the item, own and inherited combined."""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would have been nice to move it in the first commit creating the backend since they were already existing.


redundant_ids = [
access.id
for access in ItemAccess.objects.filter(item=self)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have the same problem on all its descendants... Maybe you should test that these redundant accesses are not causing any functional problem and if this is confirmed, you can clean them in an asynchronous job?

But actually I'm having second thoughts : IF after removing restriction, an access is more open than what is defined on the parent, I think the user should be warned! Because it is an open access that was not effective because of restriction and will now be effective for all users of the tree above!
So now I think everything should be reset on the whole subtree when a restriction is removed.

assert folder.link_role == "reader"


def test_models_items_restricted_uproot_moves_to_root():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

your commit message reads: "When an ancestor owner removes a restricted folder they don't
have access to"
How could I remove it if I don't see it?

Comment thread CHANGELOG.md
- ✨(frontend) add file type, contact and modification date topbar filters
- ✨(frontend) add location, file type, contact and date search filters
- ♻️(backend) route permission decisions through a swappable backend
- ✨(backend) preserve restricted folders when an ancestor is soft-deleted

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to create confusion and break things. Instead you should forbid deletion by a user if there is a restricted folder on which s.he does not have deletion rights...
Uprooting the restricted folder can be suggested but should be done explicitly... Maybe it means that owners should see restricted folders (greyed) and be able to move them out of their folders even if they have no rights on them...

Comment on lines +65 to +72
def visible(
self,
queryset: QuerySet[models.Item],
user: models.User | AnonymousUser,
path_field: str = "path",
) -> QuerySet[models.Item]:
"""Filter the queryset to the rows on which the user holds a role."""
return self.annotate_roles(queryset, user, path_field=path_field).exclude(user_roles=[])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a correct :

  • the semantic "visible" because an item may be visible through a link_reach
  • the filtering because you will exclude the items that the user can see thanks to link_reach

descendants = folder.descendants().filter(ancestors_deleted_at__isnull=True).order_by("path")
descendants = folder.descendants().filter(ancestors_deleted_at__isnull=True)
if user and user.is_authenticated:
descendants = get_permissions_backend().visible(descendants, user)

@sampaccoud sampaccoud Jul 12, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same. This is not correct because it forgets about link_reach.
This time it seems that you painted yourself in a corner as fixing this would lead to awful code and performance issues.

In the end, I think the correct way to do restriction is probably to move items that are getting restricted to the root of the tree and materialize their appearance in the origin folder by an item of type "restriction_link". By doing this, the whole access right chain will work without any modification and the item can be moved back to its location when the restriction is lifted.

before restriction

└── containing_folder (0001)
├── folder1 (00010001)
│ ├── file1 (000100010001)
│ └── file2 (000100010002)
├── folder2 (00010002)
└── file3 (00010003)

After restriction

├── folder1 (0002)
│ ├── file1 (00020001)
│ └── file2 (00020002)
└── containing_folder (0001)
├── folder1 [restriction shortcut → 0002] (00010001)
├── folder2 (00010002)
└── file3 (00010003)

Instead of modifying all the access rules and making the code buggy and complicated, you just need to handle :

  • hide it from the real root : easy since they are flagged as restricted items...
  • show them in their folder of origin : easy since you can add a shortcut of type "restriction" in the said folder

@sampaccoud

Copy link
Copy Markdown
Member

This review was made commit by commit. The first commits were fully commented as everything looked fine but at some point, the commits starting fixing caveats and everything goes wrong. The conclusion is that the method is a deadend and leads to code that is hard to understand and I'm afraid will never work (link reach was forgotten in restricted items accesses).

I ended-up suggesting another approach that should be simpler and work well : move an item to the root when it is being restricted. The review was aborted after this point.

@lterm lterm moved this to To do in La Suite Drive Jul 13, 2026
@lterm lterm moved this from To do to In Progress in La Suite Drive Jul 13, 2026
@lterm

lterm commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Thanks @sampaccoud for the review. So the alternative is to move a restricted folder to root, with a "restriction link" placeholder marking its original spot.

A few UX/UI questions this raises:

  • This restriction link ties directly into the shortcuts feature : should we plan a specific representation for links to restricted folders, distinct from regular shortcuts?
  • What does the flow look like when a user activates/deactivates the restriction ? do we walk them through what's about to happen (e.g. an info message or confirmation step explaining the folder will be moved to root)?
  • What happens when someone tries to delete a restricted folder they don't have access to?

@robin-lecomte, what do you think?

@sampaccoud

sampaccoud commented Jul 13, 2026

Copy link
Copy Markdown
Member
  • This restriction link ties directly into the shortcuts feature : should we plan a specific representation for links to restricted folders, distinct from regular shortcuts?

I could be a separate object or a shortcut flagged as "restriction" 🤔

  • What does the flow look like when a user activates/deactivates the restriction ? do we walk them through what's about to happen (e.g. an info message or confirmation step explaining the folder will be moved to root)?

You don't need to tell them about moving the item to the root (plus it will be hidden from the root..)
You probably need to explain something when a restriction is activated: that the item's permissions are going to be detached from the above tree structure and that access rights inheritance will not apply anymore. Maybe let them know that this can be reverted?
Same when deactivating, warn them that access rights will now inherit from the tree structure in which the item is located...

The reason for informing the user is that access rights on the item are changing, not that we move the item to/from the root (not his business and only a technical subject)

  • What happens when someone tries to delete a restricted folder they don't have access to?

This was discussed elsewhere in my review with several cases:

  • the user is NOT owner of the folder containing the restricted item :
    • he can only see the restricted item if he has read access to it.
    • he can only delete the restricted item if he is a owner on it.
  • the user is owner of the containing folder :
    • he can see the restricted item even without read access to it (in this case it is grey and can't be entered into)
    • he can only delete the restricted item if he is a owner on it. Eventually, he can manually detach it for good before being allowed to delete the containing folder... In the current version of the PR, this issue was solved by moving the restricted folder in order to be able to delete the containing folder... With the pattern I propose, detachment is already done and we only have to delete the restriction shortcut and reset the is_restricted flag on the now root folder ... easy!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants