Creating a child folder with is_restricted chains creation and
activation atomically, so the folder starts as a restricted root
with its shortcut in place. Only owners of the parent may use it:
a lower role would promote itself to owner through restriction.
Shortcuts themselves can never be created directly.
Shortcuts are tree entries, not content: they never match a search,
never reach the search index, and leave no entry in an exported
archive. The target itself is indexed and exported through its own
root, so users excluded from a restricted folder cannot find its
content through search or an ancestor export.
An explicit owner trashing a restricted folder leaves no entry
pointing into the trash. The folder restores as a detached root,
still restricted, reachable by its members from their listing.
Trashing a folder must not drag restricted folders with it: their
shortcuts are removed before the subtree is marked deleted, so each
target survives untouched for its members while the rest of the
branch goes to the trash as usual. Restoring the ancestor does not
bring the shortcuts back.
Deleting a shortcut removes the entry from the containing folder
without trashing anything. An owner excluded from the target acts
on the container only and can neither destroy, declassify nor read
it: the folder keeps its accesses, stays restricted, and surfaces
in its members' top-level listing.
A restricted folder lives at the tree root but its members reach it
through the shortcut when they can open the containing folder. The
listing hides the root in that case so the folder shows up in a
single location, and keeps it for members without container access.
Shortcuts expose their target's id, title and a can_access flag so
the frontend can grey out entries pointing to folders the user
cannot open. The children listing prefetches the viewer accesses to
keep the query count flat, and the tree includes shortcut entries.
Owners activate and deactivate restriction on folders via PATCH,
gated by the restrict ability. The serializer keeps working on the
instance returned by the toggle since the item physically moves.
The reach kept from the restriction period is reset to inherit when
the reattached parent already grants as much or more. A more open
explicit reach survives, matching the role normalization rule.
Explicit roles granted during restriction that are now covered by
inheritance are dropped, so the sharing screen does not keep dead
entries. Superior roles and roles without inherited counterpart stay.
The folder returns under its shortcut's current parent and the
shortcut disappears. Without a live shortcut the folder stays a
detached root. Inheritance applies again through the tree structure.
Restriction is structural: the folder physically leaves its parent
so inheritance stops applying without any query-level cut. A
shortcut materializes its origin location. Explicit link reach is
kept and defaults to restricted only when it was inherited.
Only an explicit owner can toggle restriction on a folder. A folder
needs a parent to host its shortcut on activation, while an already
restricted folder lives at the tree root and must stay deactivatable.
A shortcut materializes the original location of a restricted
folder moved to the tree root. The OneToOne target enforces a
single shortcut per folder and a DB constraint ties the target
to the shortcut type.
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.
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.
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.
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.
Move role and access resolution behind a backend resolved from
the new PERMISSIONS_BACKEND setting, following the entitlements
backend convention. Behavior is unchanged; this gives a single
decision point to swap for an ABAC engine later.
The frontend package.json now requires node >= 22, so the crowdin
workflows fail at yarn install with the node 20 they still request.
Align them with drive-frontend.yml and update the reusable job
default so future callers cannot regress silently.
Added
- ✨(backend) make the upload ACL configurable to support GCS based storages
- ✨(frontend) show the messages widget button on the homepage
- ✨(frontend) open the messages widget from the help menu
- ✨(backend) add an item batch share endpoint gated by ALLOW_SHARE_IMPORT_FILE
- ✨(frontend) share an item with contacts imported from a file
- ✨(backend) add a quota_excluded flag on items
- ✨(backend) apply per-audience attributes to external api items
- ✨(backend) add a grant_unlimited_storage command
Changed
- 🔧(docker) drop the unused pip upgrade and apk caches from the image
- ✨(backend) expose item existence in the malware detection admin
- ✨(backend) show human readable item size in the admin
- 🚚(global) move favorite items API endpoint to `/items/favorites/`
Fixed
- 🐛(docker) pin collabora image and adapt to its new runtime contract
- 🐛(backend) delete malware detection record when purging an item
- 🔒️(backend) reject unsafe filenames requested by WOPI renames
- 🔒️(backend) analyze file content written through WOPI
PutFile wrote the content straight to object storage, while malware
detection only ran on the upload endpoint. Editing a document was
therefore a way to store content that was never analysed.
The item stays READY during the analysis: a collaborator cannot open a
file that is not READY, so flipping the state would eject everyone from
the document on every save.
The requested name was decoded from UTF-7 and used as is, so a path
separator could reach the storage key and leave the item without an
extension. A second rename then granted it any extension, bypassing the
allowed list that only the upload path enforced.
Extensions are now compared case insensitively on both sides, so entries
declared with capitals are no longer dead.
Snyk Code flags the f-string path built from the request extension as
a path traversal. The DRF ChoiceField already restricts the value but
static analysis cannot follow it. Selecting the file name from a
static mapping makes the sink independent from user input, and the
serializer choices now derive from that single mapping.
The alpine 3.22 base of nginx-unprivileged carries 69 known CVEs
including a critical one in openssl (CVE-2026-31789). Renovate does
not manage Dockerfile images on this repo, so the bump is manual.
mjml v5 pulls commander@15 which requires node >= 22.12. Include the
yarn.lock and build scripts in the mail templates cache key so
dependency bumps regenerate the templates.
mjml v5 ignores mj-include by default, silently dropping the whole
mail head (Django load tag, title, fonts and styles) from the
generated templates. Opt back in.
Detection records whose item is gone can only be cleaned up manually.
Add an "item exists" column and filter to the admin so orphan records
can be spotted and deleted easily.
When an item was purged while its analysis was still pending or
processing, the detection record was left behind: the analysis kept
being relaunched and crashed on the missing file, leaving the record
stuck forever and holding a processing slot.
An empty AWS_S3_UPLOAD_ACL in an env file is easy to mistake for
an unset variable, and unset must keep meaning "private". The
explicit "default" value states the intent: let the bucket's
default object ACL apply. The frontend translates it where the
config is read, so the drivers keep sending exactly the ACL they
are given.
The upload ACL is now configurable and may be empty for storages that
do not support ACLs, so the PUT request must send exactly the ACL
signed in the upload policy. Read the value from the config endpoint
and pass it down to the S3 upload request.
GCS based storages like S3NS reject presigned PUT URLs signing the
x-amz-acl header: their S3 compatibility layer consumes x-amz-*
headers having an x-goog-* equivalent, so the signature check no
longer finds the header in the request. Objects are private by
default on S3 and GCS, so the ACL can simply be dropped by setting
AWS_S3_UPLOAD_ACL to an empty string. The setting is exposed in the
config endpoint so the frontend can send a header matching the
signature.
See https://documentation.s3ns.fr/storage/docs/aws-simple-migration
Rebase the backend image on python:3.13.14-alpine (Alpine 3.24) to
pull the patched openssl fixing CVE-2026-31789. Stays within the
pinned 3.13 runtime, so no dependency re-resolution is needed.
Next 15.5.18, Vite 6.4.3 and Turbo 2.9.14 patch several advisories:
Next.js server components denial of service (GHSA-26hh-7cqf-hhc6),
Vite server.fs.deny bypass (GHSA-fx2h-pf6j-xcff) and Turbo local
code execution on untrusted repositories (GHSA-3qcw-2rhx-2726).
Address the maintainability findings raised by the new SonarCloud
docker analyzer: drop the floating pip upgrade (nothing uses pip in
the image, the build relies on uv, and the base image already ships
a current pip), stop storing apk indexes in the image layers, quote
DRIVE_STATIC_ROOT and sort the core package list.
The collabora/code image published on 2026-07-18 (26.04.2.2.1) drops
the shell and the start script: our CMD-SHELL healthcheck could not
run at all (no /bin/sh) and extra_params was no longer read, so SSL
came back on and broke the plain-http WOPI discovery. Every e2e job
failed with "container drive-collabora-1 is unhealthy".
Pin the image and migrate to the new contract: coolwsd options go
through command arguments, and the health probe relies on the
built-in coolwsd --probe, overridden to target the http endpoint
since it defaults to https.
Added
- ✨(backend) allow converting a file while it is being analyzed
- ✨(frontend) add file type, contact and modification date topbar filters
- ✨(frontend) add location, file type, contact and date search filters
Fixed
- 🐛(nginx) let search engines see the noindex directive to drop public URLs
- 🐛(backend) find deleted root items when searching the trashbin
- 🐛(backend) exclude folders from file type search results
- 🐛(frontend) keep uploaded items usable while malware analysis runs
- 🐛(backend) stream export files from S3 without buffering
robots.txt blocked all crawling, so crawlers could never fetch the
pages and never saw the noindex meta tag: URLs discovered through
external links could still end up listed in search results.
Allow crawling in robots.txt and add a X-Robots-Tag noindex header on
frontend pages and media files so every response carries the directive.
Search rebuilds the visible set from the user's accesses, skipping those
whose item was soft deleted. A root item holds its own access, so
deleting it made it unreachable from any location, trashbin included.
Hard deleted items remain excluded, and the scope filter still keeps
deleted items out of regular search results.
The category filter keeps folders on explorer listings so navigation
stays possible. A search is not a navigation: folders have no file type
and only add noise to the results.
joserfc <1.6.8 accepts an empty HMAC key when verifying a JWT,
allowing token forgery if the secret is misconfigured (empty).
The dependency is pulled transitively via django-lasuite, which
does not enforce a minimum bound.
Add a "Quarterly" keyword spanning a my-files, a shared and a starred
item to the search fixture, and assert the location filter narrows the
search results accordingly.
runTarget only ignored "make: *** No rule to make target", missing the
"make[N]:" prefix and non-English locales. Force LC_ALL=C and match the
message loosely.
Replace the search modal type/workspace/scope filters with the location,
file type, shared-with and modification date filters, matching the new
search design.
Move the explorer filters into a dedicated filters/ folder, one file per
filter. Make the modification date filter controlled so it resets when the
active filters are cleared from outside, and share the date-range merge
logic between the topbar and the search modal.
Cover the category, contact and modification date filters, and check
that an anonymous visitor on a public folder does not get the contact
filter, whose endpoints require authentication.
Add a "Modified" filter to the topbar with Today / Last 7 days /
Last 30 days / This year presets and a compact custom date range
picker, wired to the updated_at range filter.
Add a "Shared with" filter to the topbar, listing frequent contacts
and searching people by name, wired to the contacts endpoint and the
backend contact filter.
Replace the folder/file toggle in the topbar with a file type category
filter (text document, slides, spreadsheet, pdf, image, video, audio,
archive, other), wired to the backend category filter.
Add a single location query parameter to the search endpoint, exposing
the My files, Shared with me, Starred and Trashbin views. Trashbin
overrides the scope filter as it implies a deleted scope.
Centralize creator, favorite and owner filtering as ItemQuerySet
methods, so the list filters, the trashbin endpoint and the upcoming
search location filter share a single definition.
Add a contacts endpoint on the user viewset returning the people who
share items with the current user, ranked by the number of shared
items. It mirrors the contact filter perimeter (direct and team
access, live items only) so every suggestion yields a result.
Add an updated_at date range filter on the items list so results can
be narrowed to a modification period. Bounds match on the date part,
so a date-only bound covers the whole day rather than stopping at
midnight.
Add a contact filter on the items list returning the items in which
the given user is involved through a sharing access, in either
direction, so users can find files they share with a given person.
Add a `category` filter on the items list to narrow results by file
type (text document, slides, spreadsheet, pdf, image, video, audio,
archive, other). Categories are matched on the filename extension,
since several listed formats share ambiguous mime types.
The sharing filter needs a realistic contact picker with user names.
Switching from anonymous emails to a fixed list of named users gives
the demo database the full_name and short_name fields required for
that UI.
The --file-types flag creates one fixture per category (docx, xlsx,
pptx, pdf, image, video, zip, mp3, binary) so developers can populate
a local demo database covering all filter-relevant types, without
having to upload real files manually.
default_storage.open() downloads the whole object in memory before the
first read, so exporting folders holding large files can exhaust the
worker memory. Stream straight from the boto3 response body instead.
Conversion reads the source bytes, so it must not run before malware
analysis confirms the file is safe. convert_file now waits while the
source is analyzing and drops the placeholder if it never clears.
A file uploaded by a user must stay usable during malware analysis,
including conversion. Flag the convert ability while the source is being
analyzed so the frontend offers it.
The inline dummy backend reported files safe before the upload response
returned, so the analyzing upload state was never observable. Delay the
safe result off the request thread to expose it for frontend testing.
Marking the analyzing state as transient locked freshly uploaded items in
the UI while malware detection ran. The uploader must keep full access
during analysis, so split polling from the transient UI lock and surface
the analyzing label as a non-blocking badge. The 404 conversion toast is
now limited to converting placeholders.
Added
- ✨(backend) manage reconciliation requests for user accounts
- ✨(backend) add recursive folder export as ZIP archive
- ✨(frontend) add folder export action
- ✨(backend) background conversion of legacy Office files
- ✨(backend) allow grist file upload
- ✨(frontend) add CTA on public link for anonymous and authenticated users
Changed
- 🐛(backend) replace VersionId by Etag for WOPI
- 🐛(backend) sanitize slash in template-created filenames
Removed
- 🔥(backend) drop deprecated numchild columns from item
Without the secret the request reaches OnlyOffice unsigned and fails
with a cryptic error code -8. Validate in the synchronous path before
creating the placeholder so the API returns an error immediately, and
hide the convert ability from the API so the frontend does not offer
the action.
The backend now reads the secret directly from settings, like it
already does for timeouts.
When OnlyOffice has JWT enabled it adds its own Authorization header to
outgoing file-download requests. The WOPI access token in the query
param must take precedence so the opaque token lookup succeeds.
The OnlyOffice server rejects unsigned /converter requests with error
code -8 when JWT is enabled.
Add a dedicated WOPI_ONLYOFFICE_CONVERT_JWT_SECRET setting and fix the
JWT body token format to match the OnlyOffice conversion API: encode
parameters directly instead of wrapping them in {"payload": …}, and
send only {"token": "…"} in the request body.
Enable JWT on the development OnlyOffice container so the issue is
caught locally.
The .grist extension was already in FILE_EXTENSIONS_ALLOWED but
upload failed at MIME type validation because python-magic detects
grist files as application/vnd.sqlite3.
The conversion flow crosses modal state, API calls, and explorer refreshes.
Cover the expected user path with a mocked conversion ability and placeholder
response.