Save the MFA status after the user is resolved so signed-URL downloads
inherit it, and check its age on read. Build the signing-key URL with
the URL API so it works with the vault base URL.
* fix: [OCISDEV-1476] fix AuthApp requests failing with a custom PROXY_USER_OIDC_CLAIM
* fix: [OCISDEV-1476] map ErrAccountNotFound to 401 in verifyUser
AccountResolver.verifyUser only mapped ErrAccountDisabled to 401 before
falling through to a generic 500. The identity backend excludes disabled
accounts from by-username lookups and returns ErrAccountNotFound instead,
so a disabled user authenticating via AppAuth or basic auth got a 500
instead of a 401, caught by the acceptance test suite.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix: [OCISDEV-1523] fix the LDAP master-ID filter for an empty base filter
EnhanceFilterWithMasterID replaced an empty LDAP filter with a master-ID-only
filter. This action restricted an unrestricted search to master-ID users
only. The bug broke the cross-instance user search in multi-instance
deployments. The function now keeps an empty filter unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: [OCISDEV-1523] require an explicit LDAP user filter with a master ID
EnhanceFilterWithMasterID in ocis-pkg/ldap/filter.go now keeps an empty
LDAP filter unchanged instead of restricting it to master-ID users only.
The auth-basic, users, groups, and graph services reuse this function to
compute their default LDAP user filter at startup.
An unset filter now means these services resolve or authenticate every
user in the shared LDAP directory, not only master-ID users. This action
is a behavior change for a deployment that sets
OCIS_MULTI_INSTANCE_MASTER_ID without an explicit OCIS_LDAP_USER_FILTER.
Each service now rejects startup when OCIS_MULTI_INSTANCE_MASTER_ID is
set and its own user filter is empty. This action keeps a multi-instance
deployment scoped to its own users by default.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* refactor: [OCISDEV-1523] simplify EnhanceFilterWithMasterID guard clauses
The function collapses three early-return guards into one condition.
This action removes a duplicated rationale comment between filter.go
and filter_test.go.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs: [OCISDEV-1523] point the changelog entry at the master backport PR
The changelog entry pointed at PR 13096, the stable-8.2 backport. This
commit points the entry at the master-branch PR number instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Requests on unprotected routes can carry a reva token without a resolved
user: reva's internal downloads to the data gateway (/data) are sent with
the caller's token. create_home logged "no user in context" at error level
for each of them, so every download added an error line. Log at debug with
the path instead; the request is passed on unchanged, as before.
Fixes#13101
Claude-Session: https://claude.ai/code/session_01WpoeSoKYEdgQcWjEzTVKpT
Signed-off-by: Paul Faure <paul@faure.ca>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The file and folder converters filled User from the event's Owner, which
reva does not set, so the field was empty on every upload, download, move,
delete, restore and version restore; where Owner was set it named the owner
instead of the actor. Space, user and group converters hard-coded User to
an empty string although every one of those events carries the Executant.
User now comes from the event's Executant everywhere an actor exists. The
Owner field of file events is unchanged; the failed public-link access
event has no actor and keeps an empty User.
Tests give the fixtures different owner and actor ids so they can tell the
two apart (they fail on the previous code), and add cases for the user and
group converters. The group member added/removed messages passed the user
and group ids in swapped order; fixed.
Fixes#4789
Signed-off-by: Paul Faure <paul@faure.ca>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Juan Pablo Villafañez <jpvillafanez@izertis.com>
The next oCIS release is 9.0.0, so all envvars that were annotated with
the previously planned 8.3.0 introduction version need to be updated.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(search): add a --timeout flag to the index command
ocis search index called the search service with a hard-coded 10 minute
request timeout. The service walks the whole space inside that request, so
when the timeout expired the walk was cancelled (error walking the tree:
context canceled) and everything after that point stayed unindexed. A space
whose walk takes longer than ten minutes could never be fully indexed from
the command line.
The timeout is now a flag: --timeout takes a duration, default 10m so the
behaviour is unchanged out of the box; 0 waits indefinitely (go-micro treats
a zero RequestTimeout as an already-expired deadline, so a far-off one is
substituted).
Fixes#13052
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Paul Faure <paul@faure.ca>
* docs(search): say the index command needs the running search service
The index command is a gRPC client of the search service and never opens
the Bleve index, so it works with the default exclusive lock
(SEARCH_ENGINE_BLEVE_SCALE=false) but needs the service running.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpoeSoKYEdgQcWjEzTVKpT
Signed-off-by: Paul Faure <paul@faure.ca>
---------
Signed-off-by: Paul Faure <paul@faure.ca>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* feat(audit): record tag changes in the audit log
Graph publishes TagsAdded and TagsRemoved on every tag assign and unassign,
but only the search service listened, so a tag change left no trace in the
audit log.
The audit service now registers both events and logs them as file_tags_add
and file_tags_remove, with the acting user (Executant), the space owner, the
file id and path, and the comma-separated tags. Graph sets the Timestamp on
both events, which it left unset. Two test cases use different owner and
actor ids; the audit README documents the two actions.
Fixes#13054
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Paul Faure <paul@faure.ca>
* fix(audit): record only the tags that actually changed; skip no-op tag events
Review follow-up for #13055:
- graph: TagsAdded/TagsRemoved now carry the tags that were actually added
or removed (requesting an existing tag, or removing an absent one, is not
a change). Search only uses the event's Ref, so it is unaffected.
- audit: skip tag events whose tag list is empty. Graph still publishes
TagsRemoved for search when nothing was present; that is not an audit event.
- audit: document why User is the executant for tag events; describe Tags
as carried by the event; remove a stray copy-pasted doc comment.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpoeSoKYEdgQcWjEzTVKpT
Signed-off-by: Paul Faure <paul@faure.ca>
* fix(audit): carry the requested tags in tag events, as before
Per review: TagsAdded/TagsRemoved go back to carrying the tags as
requested, and the audit log records every request, including tags that
were already present (or absent, for a removal), so repeated requests stay
visible. Computing the actual delta belongs in the tags package, not here.
Removes the graph-side tagsDiff helper and its test, and the audit
service's skip for empty tag lists. The other review fixes stay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpoeSoKYEdgQcWjEzTVKpT
Signed-off-by: Paul Faure <paul@faure.ca>
---------
Signed-off-by: Paul Faure <paul@faure.ca>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(storage-users): [OCISDEV-1382] list upload sessions through the coordinator
Signed-off-by: Firas Frikha <firas.frikha@kiteworks.com>
* fix(storage-users): [OCISDEV-1382] use a safe type assertion for the driver config
Signed-off-by: Firas Frikha <firas.frikha@kiteworks.com>
---------
Signed-off-by: Firas Frikha <firas.frikha@kiteworks.com>
Remove the "Session expiring" extend/dismiss popup and its expiry
worker. Drop the frontend session_duration capability and config;
OCIS_MFA_SESSION_DURATION stays in use by the proxy.
* chore(OCISDEV-900): bump reva to pr7-switchover, add upload_directory and mount_id to revaconfig
* chore(OCISDEV-900): bump reva to the switchover rebased on the upload write fix
* chore(OCISDEV-900): bump reva to main with the upload coordinator switchover
---------
Co-authored-by: Firas Frikha <firas.frikha@kiteworks.com>
* fix: the public share configuration and fixed graceful shutdown
* fix: [OCISDEV-1433] remove GRPC_MAX_CONNECTION_AGE
The setting closed healthy grpc connections on a timer so that clients would redo
their DNS lookup. It never helped against the failure it was reached for - a
connection whose peer stopped answering - because the age limit is enforced by
the server, the grace period was left at infinity so a request already in flight
was never ended, and any value without a unit suffix silently disabled it again.
Detecting a peer that stops answering is now the job of the grpc clients, which
ping the peer while a request is in flight. The servers here got the matching
enforcement policy so they accept those pings; the client side arrives with the
next reva bump.
* fix: Added the new variable OCDAV_DISABLE_PROPFIND_PUBLIC_LINK_RESOLUTION that disable the resolution of public link shares when answering PROPFIND requests.
* chore: bump reva
* fix: The keepalive added to grpc clinet
* chore: bump reva
* build(deps): bump go 1.26.8, grpc-go dependency
---------
(cherry picked from commit e86eb4d210)
For the 9.0 branch, the reva dependency was pointed at reva's main-branch
merge of PR #748 (commit 373f9509a, pseudo-version
v2.0.0-20261005091647-373f9509a363) instead of the 8.2-targeted
e0beaa380005, since the 8.2 merge predates the VaultMode/UploadSessionFilter.Orphaned
APIs already present on the 9.0 reva lineage.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts:
# changelog/unreleased/security-bump-grpc.md
# go.mod
# go.sum
# vendor/google.golang.org/grpc/server.go
# vendor/google.golang.org/grpc/stream.go
# vendor/google.golang.org/grpc/version.go
# vendor/modules.txt
`requireAcr()`, used by the admin settings route guards, redirected to the
IdP whenever the `acr` did not match, so a user without a second factor
looped forever.
It now uses the same protection as the vault: the step-up carries its
target as OIDC state, and if the IdP answers it without the required
`acr`, `requireAcr()` returns false. The admin settings guards then
redirect to the default view and an error message is shown.
Follow-up to https://github.com/owncloud/ocis/pull/12985, which fixed the same loop for the vault.
* chore: bump reva
* chore: bump pnpm dependencies
* fix: avoid go vet false positive in userlog conversion.go
The go.mod bump to go 1.26.8 made go vet flag loadTemplates' calls to
t.Get(nt.Subject/nt.Message) as non-constant format strings, breaking
the build for services/userlog/pkg/service. These are translation
lookup keys, not printf format strings, so route the call through a
local func value to defeat vet's static trace.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* chore: bump grpc
Fixes GO-2026-6443, a server panic when handling requests missing
authority or Host headers, flagged by govulncheck in CI.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Selecting a language dropped focus to the page body and the save
confirmation went unannounced, because the select's own best-effort
refocus raced against the handler's translation reload and network
save. Restore focus explicitly once that work settles, before
announcing the save.
* fix(web): stop redirect loop when vault MFA step-up fails
Signed-off-by: Samuel Schumacher <samuel.schumacher@werkspiel.de>
Signed-off-by: Samuel Schumacher <schumacher@b1-systems.de>
* fix(web): leave vault with a full page load after failed MFA step-up
Returning to `/` in-app kept the vault clients and loaded vault spaces
with the non-MFA token, which ended on the RUNTIME-BOOTSTRAP-040 crash
page. Vault mode is fixed per page load, so reload outside the vault
instead and show the MFA error message after the reload. The spaces
load error handler no longer routes to the crash page while leaving.
Also addresses review feedback:
- use $pgettext with translator context for the error message
- test that an expired user with a fresh marker is still redirected
* fix(web): only treat a vault step-up as failed after the IdP answered it
The step-up marker relied on a 5 minute TTL, so a user who went to the
IdP, pressed back and opened the vault again got the error without ever
seeing the IdP. The step-up request now carries its target as OIDC
state; the sign-in callback picks it up and only the navigation right
after the callback counts as the answer. The sessionStorage marker and
the TTL are gone.
Also addresses review feedback:
- simplify the auth guard check to `redirect !== undefined`
- drop the storage key re-export, the spec imports it from the helper
Signed-off-by: Samuel Schumacher <schumacher@b1-systems.de>
---------
Signed-off-by: Samuel Schumacher <samuel.schumacher@werkspiel.de>
Signed-off-by: Samuel Schumacher <schumacher@b1-systems.de>
* fix(web): [OCISDEV-1478] move combobox role to the focusable input
The combobox role and its ARIA state lived on vue-select's non-focusable
wrapper div, so screen readers never announced the actual input as a
combobox and its accessible name fell back to a generic hardcoded label.
Moves role, aria-expanded, aria-controls, aria-haspopup and
aria-activedescendant to the focusable search input and gives it its own
label.
* fix(web): [OCISDEV-1478] fix CI, drop QuotaSelect's own search slot
It rendered a plain unlabeled input, bypassing OcSelect's accessible
one. That only worked by accident because vue-select's wrapper div
used to carry a fallback label, which the combobox fix removes.
feat(web): adopt Nx as task orchestrator for web/
Introduces Nx on top of the existing pnpm workspace, so lint/typecheck/
test can be scoped to only the projects affected by a change instead of
running against the whole monorepo on every PR.
- Phase 0: add nx (^23.2.1) as a workspace devDependency, nx.json with a
targetDefaults entry for test:unit (cache: true), ignore .nx/cache and
.nx/workspace-data. No behavior change to any existing script.
- Phase 1: register @nx/vitest's inference plugin so every package with
a vitest.config.ts gets a real, cached "test" target read directly
from its own config, not just the packages that happened to already
declare a "test:unit" npm script. Excludes vite.config.common.ts,
the root vite.config.ts and vite.cern.config.ts from the plugin's
default glob - none of them are test configs, and loading them as one
aborts the whole project graph. defaultBase: "master" (bare `nx
affected` defaults to a branch named "main"). testMode: "run" so `nx
run <project>:test` doesn't default to vitest's watch mode locally.
- Phase 2a: register @nx/eslint/plugin for a per-project "lint" target
on every project (root `lint` script untouched).
- Phase 2b: register @nx/vite/plugin's typecheckTargetName for a
per-project "typecheck" target on projects with their own real
vite.config.ts (design-system, web-client, web-pkg, web-test-helpers).
The ~20 web-app-*/web-runtime projects only have a vitest.config.ts
and aren't covered; root `pnpm run check:types` (whole-repo vue-tsc)
stays the one check that covers everything.
- Phase 3 (CI): adds a `full-run` output to detect-web-changes - true for
push to master/stable-* or a `[full-ci]` PR title (unchanged behavior),
false for a normal PR, which instead runs `nx affected -t lint` / `nx
affected -t test --base=<PR base sha>`. Added fetch-depth: 0 to
web-build-test's checkout, needed for `nx affected --base=<sha>` to
find that commit. Check types and the full-run unit-test path
deliberately stay whole-repo (see inline comments for why).
Also:
- removes design-system's broken "lint": "lint:stylelint" alias (needed
the "lint" name free for the new per-project target; it was never
callable as written - "lint:stylelint" isn't a binary - and nothing
called it).
- makes the design system's webfontloader import fire-and-forget so an
in-flight rejection at test teardown doesn't surface as an unhandled
rejection under vitest's per-project runs.
- pin `typescript` via `pnpm.overrides`. Adding nx's own devDependencies
makes pnpm re-resolve the workspace's peer graph, and today's registry
now has a typescript 6.x that some peer ranges accept - without the
pin, half the vue-peer-dependent packages (pinia, vue-router,
vue3-gettext, focus-trap-vue, ...) fork into a second, incompatible
module instance. Same pin as the dependency-cycle fix, for an
unrelated reason; needed independently here since this branch changes
package.json on its own.
nx's `dependsOn: ["^target"]` errors outright on a circular task graph,
so this only works once the pre-existing web-pkg/design-system/
web-test-helpers dependency cycle is fixed - see
fix(web): break web-pkg/design-system/web-test-helpers dependency cycle,
which this PR targets rather than master.
Verified: nx run-many -t test / lint / typecheck succeed for all
projects; nx show projects --affected --files=... resolves correctly for
shared config changes; nx affected -t lint --base=<sha> matches pnpm
lint; root pnpm lint/check:types/check:format/test:unit/build all still
pass unchanged (380 files / 3128 tests).
fix(web): break web-pkg/design-system/web-test-helpers dependency cycle
web-pkg, design-system and web-test-helpers formed a cycle: the mount
helpers have to install the design system (its components are registered
globally) and web-pkg's mocked pinia stores, so web-test-helpers was
imported by both packages that imported it back.
Resolved by layering the helpers onto the packages that already need
them, instead of adding another one:
design-system/testing mount/shallowMount, prop types, design system +
gettext + router-link stub plugins
web-pkg/src/testing the above, extended with casl abilities, mocked
pinia stores and component mocks
web-test-helpers re-export of web-pkg/src/testing
design-system sits at the bottom of the workspace graph, so it cannot
depend on a package that installs it - hence the generic half lives
there. web-pkg composes those plugins rather than duplicating them, so
every helper is still defined exactly once, and no package depends on
one above it.
The public API of `@ownclouders/web-test-helpers` is unchanged, so
external extensions are unaffected.
Also:
- pin `typescript` via `pnpm.overrides` - without it, a second peer-
resolved typescript version forks `vue`, which forks `vue3-gettext`
into two module instances, breaking `inject(GetTextSymbol)` for any
component mounted through these helpers.
- keep `@ownclouders/design-system/testing` external in web-pkg's lib
build. rollup matches `external` strings exactly, so the subpath was
inlined, shipping a second copy of the design system and of
@vue/test-utils into the published bundle - components and wrappers
were then not identity-equal to the consumer's own, and gettext
injection broke.
- name the web-test-helpers umd output `.cjs`. The package is ESM, so the
`.js` file that publishConfig.exports pointed `require` at could not be
required.
Verified: `pnpm install --frozen-lockfile` clean, zero cyclic-workspace
warnings; `pnpm test:unit` 380 files / 3129 tests pass; `pnpm check:types`
0 errors; `pnpm lint`/`check:format` 0 errors; `pnpm build` succeeds;
published artifacts checked by mounting through the built `dist/` bundles
the way an external extension resolves them via publishConfig.exports.
* test:start auth-bearer service separately instead of via OCIS_ADD_RUN_SERVICES
* test: fix timezone-dependent expiry assertions and flaky email notification
* test: simplify host timezone detection and log the UTC fallback
* test: oonly run cli commands test suite
* test: run all suites
* test: resolve the expiry timestamp in the timezone the mail was sent in
* test: inline the UTC expiration date conversion and drop the helper
* test: simplify the mail timezone formatter and restore the CI coverage guard
* fix(web): [OCISDEV-1474] keep focus on trigger when dropdown closes
Dropdowns that close on item click removed the clicked item from the
DOM, dropping keyboard focus to the page body and causing screen
readers to re-announce the whole page over any status message the
action triggered. OcDrop now restores focus to its trigger on close,
unless the action already moved focus elsewhere itself.
* fix(web): [OCISDEV-1474] hide trigger tooltip when refocused on close
Restoring focus to the trigger button popped its own tooltip back
open, since tippy's default trigger includes focus. The tooltip then
sat on top of the button and blocked the next click, breaking the
share e2e test. Hide the tooltip right after refocusing.
Toast/status messages (e.g. clipboard copy, file restore) were often
dropped by screen readers because the live region was never cleared
between messages. Clear it before each message and refill after a
short delay so announcements survive focus changes. Toasts themselves
are no longer live regions, since a freshly inserted aria-live node
isn't reliably announced.
Searching for share recipients in the vault ("Safe") could take minutes,
because each search scanned the role assignments of every account in the
system. Vault eligibility is now checked only for the users the search
matched, a few at a time in parallel, so the search is about as fast as
outside the vault.