From 222c101fe89cfba02f7a18747059990948168dd3 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Thu, 6 Aug 2026 10:12:48 +0200 Subject: [PATCH 1/8] fix userlog policy violation/antivirus event handling, enabled granular notifications --- .vscode/launch.json | 3 ++- devtools/deployments/opencloud_full/opencloud.yml | 1 + services/userlog/pkg/service/filter.go | 2 ++ services/userlog/pkg/service/filter_test.go | 6 ++++++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index bad129046e..7103226f0f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -73,7 +73,8 @@ // service accounts "OC_SERVICE_ACCOUNT_ID": "service-account-id", - "OC_SERVICE_ACCOUNT_SECRET": "service-account-secret" + "OC_SERVICE_ACCOUNT_SECRET": "service-account-secret", + "FRONTEND_CONFIGURABLE_NOTIFICATIONS": "true" } }, { diff --git a/devtools/deployments/opencloud_full/opencloud.yml b/devtools/deployments/opencloud_full/opencloud.yml index 1c8790dcf2..d6dae17371 100644 --- a/devtools/deployments/opencloud_full/opencloud.yml +++ b/devtools/deployments/opencloud_full/opencloud.yml @@ -58,6 +58,7 @@ services: COMPANION_DOMAIN: ${COMPANION_DOMAIN:-companion.opencloud.test} # enable to allow using the banned passwords list OC_PASSWORD_POLICY_BANNED_PASSWORDS_LIST: banned-password-list.txt + FRONTEND_CONFIGURABLE_NOTIFICATIONS: "true" volumes: - ./config/opencloud/app-registry.yaml:/etc/opencloud/app-registry.yaml - ./config/opencloud/csp.yaml:/etc/opencloud/csp.yaml diff --git a/services/userlog/pkg/service/filter.go b/services/userlog/pkg/service/filter.go index f45ed18a8d..6676c4f842 100644 --- a/services/userlog/pkg/service/filter.go +++ b/services/userlog/pkg/service/filter.go @@ -65,6 +65,8 @@ func (ulf userlogFilter) filterUsersBySettings(ctx context.Context, users []stri settingId = defaults.SettingUUIDProfileEventSpaceDeleted case ocEvents.ResourceMention: settingId = defaults.SettingUUIDProfileEventResourceMention + case events.PostprocessingStepFinished: + settingId = defaults.SettingUUIDProfileEventPostprocessingStepFinished default: // event that cannot be disabled return users diff --git a/services/userlog/pkg/service/filter_test.go b/services/userlog/pkg/service/filter_test.go index 1cfac22a9e..04c2f4ea5c 100644 --- a/services/userlog/pkg/service/filter_test.go +++ b/services/userlog/pkg/service/filter_test.go @@ -174,5 +174,11 @@ var _ = Describe("NotificationFilter", func() { Expect(ulf.execute(context.TODO(), events.Event{Event: events.SpaceDeleted{}}, nil, []string{"foo"})).To(BeEmpty()) }) + + It("handles PostprocessingStepFinished disabled", func() { + ulf.valueClient = setupMockValueService(false) + + Expect(ulf.execute(context.TODO(), events.Event{Event: events.PostprocessingStepFinished{}}, nil, []string{"foo"})).To(BeEmpty()) + }) }) }) From ffd33baff84eb371a2e97310ea9a7b648eb2e0b7 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Thu, 6 Aug 2026 10:59:35 +0200 Subject: [PATCH 2/8] removed env vars for granular notifications from docker and vscode config --- .vscode/launch.json | 3 +-- devtools/deployments/opencloud_full/opencloud.yml | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 7103226f0f..bad129046e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -73,8 +73,7 @@ // service accounts "OC_SERVICE_ACCOUNT_ID": "service-account-id", - "OC_SERVICE_ACCOUNT_SECRET": "service-account-secret", - "FRONTEND_CONFIGURABLE_NOTIFICATIONS": "true" + "OC_SERVICE_ACCOUNT_SECRET": "service-account-secret" } }, { diff --git a/devtools/deployments/opencloud_full/opencloud.yml b/devtools/deployments/opencloud_full/opencloud.yml index d6dae17371..1c8790dcf2 100644 --- a/devtools/deployments/opencloud_full/opencloud.yml +++ b/devtools/deployments/opencloud_full/opencloud.yml @@ -58,7 +58,6 @@ services: COMPANION_DOMAIN: ${COMPANION_DOMAIN:-companion.opencloud.test} # enable to allow using the banned passwords list OC_PASSWORD_POLICY_BANNED_PASSWORDS_LIST: banned-password-list.txt - FRONTEND_CONFIGURABLE_NOTIFICATIONS: "true" volumes: - ./config/opencloud/app-registry.yaml:/etc/opencloud/app-registry.yaml - ./config/opencloud/csp.yaml:/etc/opencloud/csp.yaml From 6e552b30e2dd78ff9c2d27164ea4160d194f89f7 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Wed, 12 Aug 2026 21:50:21 +0200 Subject: [PATCH 3/8] bump reva version with backport and fixes for SpaceMembershipExpired --- go.mod | 6 +- go.sum | 12 +- vendor/github.com/go-git/go-git/v5/.gitignore | 1 + .../go-git/go-git/v5/plumbing/reference.go | 42 +++++ .../v5/storage/filesystem/dotgit/dotgit.go | 57 +++++++ .../github.com/go-git/go-git/v5/worktree.go | 67 ++++++++ .../go-git/go-git/v5/worktree_fs.go | 115 +++++++++++-- .../go-git/go-git/v5/worktree_status.go | 2 +- .../sharesstorageprovider.go | 1 + .../v2/pkg/share/manager/jsoncs3/jsoncs3.go | 157 +++++++++++++----- .../pkg/storage/utils/decomposedfs/spaces.go | 61 +++++-- vendor/modules.txt | 6 +- 12 files changed, 437 insertions(+), 90 deletions(-) diff --git a/go.mod b/go.mod index d2fadc4d45..60013878d4 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( github.com/open-policy-agent/opa v1.19.0 github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d - github.com/opencloud-eu/reva/v2 v2.48.0 + github.com/opencloud-eu/reva/v2 v2.48.1-0.20260812174127-12b756ed83c5 github.com/opensearch-project/opensearch-go/v4 v4.6.0 github.com/orcaman/concurrent-map v1.0.0 github.com/pkg/errors v0.9.1 @@ -72,7 +72,7 @@ require ( github.com/prometheus/client_golang v1.24.1 github.com/r3labs/sse/v2 v2.10.0 github.com/riandyrn/otelchi v0.12.3 - github.com/rogpeppe/go-internal v1.15.0 + github.com/rogpeppe/go-internal v1.16.0 github.com/rs/cors v1.11.1 github.com/rs/zerolog v1.35.1 github.com/shamaton/msgpack/v2 v2.4.1 @@ -206,7 +206,7 @@ require ( github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.9.0 // indirect - github.com/go-git/go-git/v5 v5.19.1 // indirect + github.com/go-git/go-git/v5 v5.19.2 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect diff --git a/go.sum b/go.sum index e3518668b1..754464811b 100644 --- a/go.sum +++ b/go.sum @@ -383,8 +383,8 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= -github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY= +github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -942,8 +942,8 @@ github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+l github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89/go.mod h1:vigJkNss1N2QEceCuNw/ullDehncuJNFB6mEnzfq9UI= github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d h1:JcqGDiyrcaQwVyV861TUyQgO7uEmsjkhfm7aQd84dOw= github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q= -github.com/opencloud-eu/reva/v2 v2.48.0 h1:G/4Jbv0DWWOfA5u5DtV0CB75pi9Wwtj7JkJQOEBvErs= -github.com/opencloud-eu/reva/v2 v2.48.0/go.mod h1:ZCo/xQM6if+upZa7rJCmdifZ/Y5XHLCrscHytC39yI4= +github.com/opencloud-eu/reva/v2 v2.48.1-0.20260812174127-12b756ed83c5 h1:Lg+7ZFXFi4lLOTjch99QYLp71sRkXunsOwxsrTaheGA= +github.com/opencloud-eu/reva/v2 v2.48.1-0.20260812174127-12b756ed83c5/go.mod h1:+2IJFVwi3yBEBfw0T+jgY7M9LBSPv7ecPYDiG3/bWYg= github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI= github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -1063,8 +1063,8 @@ github.com/riandyrn/otelchi v0.12.3 h1:KW9gA+97d6mExk8vbh0FRwb2biUvpyYlc8YuxP1Oa github.com/riandyrn/otelchi v0.12.3/go.mod h1:weZZeUJURvtCcbWsdb7Y6F8KFZGedJlSrgUjq9VirV8= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= -github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= +github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g= +github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= diff --git a/vendor/github.com/go-git/go-git/v5/.gitignore b/vendor/github.com/go-git/go-git/v5/.gitignore index b7f2c5807c..62dae67e82 100644 --- a/vendor/github.com/go-git/go-git/v5/.gitignore +++ b/vendor/github.com/go-git/go-git/v5/.gitignore @@ -5,3 +5,4 @@ profile.out .tmp/ .git-dist/ .vscode +build/tools/ diff --git a/vendor/github.com/go-git/go-git/v5/plumbing/reference.go b/vendor/github.com/go-git/go-git/v5/plumbing/reference.go index 4daa341649..9d34135b2e 100644 --- a/vendor/github.com/go-git/go-git/v5/plumbing/reference.go +++ b/vendor/github.com/go-git/go-git/v5/plumbing/reference.go @@ -110,6 +110,48 @@ func (r ReferenceName) IsTag() bool { return strings.HasPrefix(string(r), refTagPrefix) } +// IsSafe reports whether the reference name can be safely turned into a path +// under the .git directory, mirroring Git's refname_is_safe (refs.c). A name +// is safe when it is either: +// +// - under "refs/", non-empty after the prefix, containing no backslash and +// no empty, "." or ".." path component (so it cannot escape the refs/ +// sub-tree, or alias another name, once turned into a path); or +// - a one-level pseudo-ref whose spelling is restricted to [A-Z_] +// (e.g. HEAD, ORIG_HEAD, FETCH_HEAD). +// +// Everything else — a lowercase or mixed one-level name such as "config" or +// "index", an absolute or drive-prefixed name, or a refs/ name that escapes — +// is unsafe, because it could resolve onto unrelated repository metadata. +func (r ReferenceName) IsSafe() bool { + s := string(r) + if s == "" { + return false + } + + if rest, ok := strings.CutPrefix(s, refPrefix); ok { + // '\' is a path separator on Windows, so a refs/ name containing one + // could escape the sub-tree or alias another name once turned into a + // path; reject it outright (check_refname_format forbids '\' too). + if rest == "" || strings.Contains(rest, "\\") { + return false + } + for part := range strings.SplitSeq(rest, "/") { + if part == "" || part == "." || part == ".." { + return false + } + } + return true + } + + for i := 0; i < len(s); i++ { + if (s[i] < 'A' || s[i] > 'Z') && s[i] != '_' { + return false + } + } + return true +} + func (r ReferenceName) String() string { return string(r) } diff --git a/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/dotgit.go b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/dotgit.go index eb85a11454..d57a6dc82b 100644 --- a/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/dotgit.go +++ b/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/dotgit.go @@ -16,6 +16,7 @@ import ( "strings" "time" + "github.com/go-git/go-git/v5/internal/pathutil" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/hash" "github.com/go-git/go-git/v5/storage" @@ -79,8 +80,52 @@ var ( // resolve outside the modules/ subtree, mirroring canonical Git's // "ignoring suspicious submodule name" defence. ErrModuleNameEscape = errors.New("submodule name escapes modules/ directory") + // ErrReferenceNameEscape is returned when a reference name would + // resolve outside its reference sub-tree once turned into a path + // under the .git directory (e.g. a name with a ".." component). + ErrReferenceNameEscape = errors.New("reference name escapes the reference storage") ) +// isPathSep reports whether r is a path separator in reference names. +// It treats both '/' and '\\' as separators to harden against cross-OS paths. +func isPathSep(r rune) bool { return r == '/' || r == '\\' } + +// validReferenceName rejects reference names that cannot be safely turned into +// a path under the .git directory. A loose reference is stored verbatim at +// ".git/", so a crafted name — for instance one advertised by a malicious +// remote — could climb out of its reference sub-tree and read, overwrite, or +// delete unrelated metadata such as .git/config. +// +// The storage-safety gate is plumbing.ReferenceName.IsSafe, mirroring Git's +// refname_is_safe: a name must be under refs/ without escaping it, or be a +// [A-Z_] pseudo-ref. This alone rejects absolute, drive-prefixed, escaping and +// single-level metadata names. On top of it, this adds filesystem-specific +// hardening that IsSafe's literal check does not cover: control characters, and +// components a case-insensitive/NTFS/HFS+ filesystem would fold back to "." or +// ".." (trailing dots/spaces, Alternate Data Streams, ignorable Unicode code +// points), delegated to pathutil.IsHFSDot and pathutil.IsNTFSDot with "." as +// the needle — as validSubmoduleName does — and run regardless of host OS. +func validReferenceName(name plumbing.ReferenceName) error { + if !name.IsSafe() { + return fmt.Errorf("%w: %q is not under refs/ nor a valid pseudo-ref", ErrReferenceNameEscape, string(name)) + } + + s := string(name) + for i := 0; i < len(s); i++ { + if s[i] < 0x20 || s[i] == 0x7f { + return fmt.Errorf("%w: %q", ErrReferenceNameEscape, s) + } + } + for _, part := range strings.FieldsFunc(s, isPathSep) { + // IsNTFSDot/IsHFSDot with a "." needle match ".." and its disguises + // but not a bare ".", so reject that component explicitly too. + if part == "." || pathutil.IsHFSDot(part, ".") || pathutil.IsNTFSDot(part, ".", "") { + return fmt.Errorf("%w: %q", ErrReferenceNameEscape, s) + } + } + return nil +} + // Options holds configuration for the storage. type Options struct { // ExclusiveAccess means that the filesystem is not modified externally @@ -706,6 +751,10 @@ func (d *DotGit) checkReferenceAndTruncate(f billy.File, old *plumbing.Reference } func (d *DotGit) SetRef(r, old *plumbing.Reference) error { + if err := validReferenceName(r.Name()); err != nil { + return err + } + var content string switch r.Type() { case plumbing.SymbolicReference: @@ -741,6 +790,10 @@ func (d *DotGit) Refs() ([]*plumbing.Reference, error) { // Ref returns the reference for a given reference name. func (d *DotGit) Ref(name plumbing.ReferenceName) (*plumbing.Reference, error) { + if err := validReferenceName(name); err != nil { + return nil, err + } + ref, err := d.readReferenceFile(".", name.String()) if err == nil { return ref, nil @@ -804,6 +857,10 @@ func (d *DotGit) packedRef(name plumbing.ReferenceName) (*plumbing.Reference, er // RemoveRef removes a reference by name. func (d *DotGit) RemoveRef(name plumbing.ReferenceName) error { + if err := validReferenceName(name); err != nil { + return err + } + path := d.fs.Join(".", name.String()) _, err := d.fs.Stat(path) if err == nil { diff --git a/vendor/github.com/go-git/go-git/v5/worktree.go b/vendor/github.com/go-git/go-git/v5/worktree.go index d8ee9fdd13..b935c6b783 100644 --- a/vendor/github.com/go-git/go-git/v5/worktree.go +++ b/vendor/github.com/go-git/go-git/v5/worktree.go @@ -583,6 +583,10 @@ func (w *Worktree) checkoutChangeSubmodule(name string, return err } + if err := w.clearBlockingSymlinks(name); err != nil { + return err + } + if err := w.Filesystem.MkdirAll(name, mode); err != nil { return err } @@ -626,7 +630,70 @@ func (w *Worktree) checkoutChangeRegularFile(name string, return nil } +// clearBlockingSymlinks removes a symlink that is in the way of +// materialising name, so the checkout writes a real entry in its place +// instead of following the link out of the worktree. Two cases: +// +// - a leading directory component that is a symlink (e.g. "s" while +// writing "s/config", where "s" links to ".git"): OpenFile/MkdirAll +// would traverse it, so the write would land under the link's target. +// - the final component itself being a symlink (e.g. writing "s" while +// "s" links to ".git/config"): OpenFile with O_TRUNC, or Symlink, +// would follow/replace through it and clobber the target. +// +// A symlink can never be a legitimate parent of, or the destination for, +// a tracked entry, so removing it is always correct. This mirrors upstream +// Git's forced checkout, which unlinks a blocking symlink in the leading +// path (create_directories) and unlinks an existing entry before +// write_entry. +// https://github.com/git/git/blob/v2.54.0/entry.c#L50 +func (w *Worktree) clearBlockingSymlinks(name string) error { + var dirs []string + for dir := filepath.Dir(name); dir != "." && dir != "" && dir != string(filepath.Separator); dir = filepath.Dir(dir) { + dirs = append(dirs, dir) + } + // Leading components, shallowest-first: removing the shallowest symlink + // invalidates every component beneath it, so a single removal is enough. + for i := len(dirs) - 1; i >= 0; i-- { + fi, err := w.Filesystem.Lstat(dirs[i]) + if err != nil { + // A missing component is created as a real directory by the + // checkout. Any other error means we cannot tell whether it is + // a symlink, so surface it instead of leaving a blocking link in + // place and failing later in a harder-to-diagnose way. + if os.IsNotExist(err) { + continue + } + return err + } + if fi.Mode()&os.ModeSymlink != 0 { + return w.Filesystem.Remove(dirs[i]) + } + } + // Final component: an existing symlink here would be followed by the + // subsequent OpenFile/Symlink/MkdirAll, so replace it. + fi, err := w.Filesystem.Lstat(name) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + if fi.Mode()&os.ModeSymlink != 0 { + return w.Filesystem.Remove(name) + } + return nil +} + func (w *Worktree) checkoutFile(f *object.File) (err error) { + // checkoutFile is the materialisation boundary for tracked entries. + // Remove any blocking symlink first so the subsequent OpenFile or + // Symlink call writes the entry itself instead of following a planted + // final-component link in the underlying filesystem. + if err := w.clearBlockingSymlinks(f.Name); err != nil { + return err + } + mode, err := f.Mode.ToOSFileMode() if err != nil { return diff --git a/vendor/github.com/go-git/go-git/v5/worktree_fs.go b/vendor/github.com/go-git/go-git/v5/worktree_fs.go index 9bc2fd97dc..cf16de746a 100644 --- a/vendor/github.com/go-git/go-git/v5/worktree_fs.go +++ b/vendor/github.com/go-git/go-git/v5/worktree_fs.go @@ -35,10 +35,25 @@ func defaultProtectNTFS() bool { return true } -// worktreeFilesystem wraps a billy.Filesystem and validates every path passed -// to a mutating operation. This prevents writing to, or deleting from, -// dangerous locations (e.g. .git/*, ../) regardless of which worktree -// code path triggers the operation. +// worktreeFilesystem wraps a billy.Filesystem and validates every path it +// is handed, so worktree operations cannot use dangerous paths at the +// boundary. Two layers apply: +// +// - validPath rejects dangerous path *strings*: .git and its HFS+/NTFS +// variants, "..", control characters, volume names. +// - validNoLeadingSymlink rejects paths whose leading directories +// already exist on disk as symlinks, so a write or delete cannot +// follow a planted link out of the tree. +// +// Both layers run on every mutating operation (validWritePath) and every +// read (validReadPath). Chroot additionally refuses a symlink as the final +// component, so a sub-filesystem such as a submodule worktree cannot be +// scoped to a redirected target. +// +// The wrapper intentionally stops at leading-component traversal. Callers +// that need final-component no-follow semantics for materialisation +// (checkoutFile) enforce that directly by removing the blocking symlink +// before opening the destination path. type worktreeFilesystem struct { billy.Filesystem protectNTFS bool @@ -50,7 +65,7 @@ func newWorktreeFilesystem(fs billy.Filesystem, protectNTFS, protectHFS bool) *w } func (sfs *worktreeFilesystem) Create(filename string) (billy.File, error) { - if err := sfs.validPath(filename); err != nil { + if err := sfs.validWritePath(filename); err != nil { return nil, fmt.Errorf("create: %w", err) } return sfs.Filesystem.Create(filename) @@ -64,7 +79,7 @@ func (sfs *worktreeFilesystem) Open(filename string) (billy.File, error) { } func (sfs *worktreeFilesystem) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) { - if err := sfs.validPath(filename); err != nil { + if err := sfs.validWritePath(filename); err != nil { return nil, fmt.Errorf("openfile: %w", err) } return sfs.Filesystem.OpenFile(filename, flag, perm) @@ -78,14 +93,14 @@ func (sfs *worktreeFilesystem) Stat(filename string) (os.FileInfo, error) { } func (sfs *worktreeFilesystem) Remove(filename string) error { - if err := sfs.validPath(filename); err != nil { + if err := sfs.validWritePath(filename); err != nil { return fmt.Errorf("remove: %w", err) } return sfs.Filesystem.Remove(filename) } func (sfs *worktreeFilesystem) Rename(from, to string) error { - if err := sfs.validPath(from, to); err != nil { + if err := sfs.validWritePath(from, to); err != nil { return fmt.Errorf("rename: %w", err) } return sfs.Filesystem.Rename(from, to) @@ -106,7 +121,7 @@ func (sfs *worktreeFilesystem) Lstat(filename string) (os.FileInfo, error) { } func (sfs *worktreeFilesystem) Symlink(target, link string) error { - if err := sfs.validPath(link); err != nil { + if err := sfs.validWritePath(link); err != nil { return fmt.Errorf("symlink: %w", err) } if err := sfs.validSymlinkName(link); err != nil { @@ -131,7 +146,7 @@ func (sfs *worktreeFilesystem) MkdirAll(path string, perm os.FileMode) error { if path == "" || path == "." || path == "/" { return nil } - if err := sfs.validPath(path); err != nil { + if err := sfs.validWritePath(path); err != nil { return fmt.Errorf("mkdirall: %w", err) } return sfs.Filesystem.MkdirAll(path, perm) @@ -145,18 +160,39 @@ func (sfs *worktreeFilesystem) Chroot(path string) (billy.Filesystem, error) { if err := sfs.validReadPath(path); err != nil { return nil, fmt.Errorf("chroot: %w", err) } + // Chroot scopes a sub-filesystem to path, so the final component must + // be a real directory too: a symlink there would silently redirect the + // scope (e.g. a submodule worktree) to a target outside the tree. This + // is the "valid path, wrong target" case that validNoLeadingSymlink, + // which only inspects leading components, does not cover. + // + // A non-existent target is fine: Chroot creates it as a real + // directory. Any other Lstat error means we cannot prove the target + // is not a symlink, so fail closed rather than scope through it. + if fi, err := sfs.Filesystem.Lstat(path); err != nil { + if !os.IsNotExist(err) { + return nil, fmt.Errorf("chroot: cannot stat %q: %w", path, err) + } + } else if fi.Mode()&os.ModeSymlink != 0 { + return nil, fmt.Errorf("chroot: invalid path %q: is a symlink", path) + } return sfs.Filesystem.Chroot(path) } -// validReadPath is like validPath but treats the empty string and "." as -// valid references to the worktree root. Read-side operations on the root -// (e.g. ReadDir(""), Lstat(".")) are legitimate; mutating the root itself -// is not, so write-side operations continue to use validPath directly. +// validReadPath is like validWritePath but treats the empty string and "." +// as valid references to the worktree root. Read-side operations on the +// root (e.g. ReadDir(""), Lstat(".")) are legitimate. Mutating the root +// itself is not, so write-side operations reject it via validPath. Reads +// are still refused through a leading symlink, so the wrapper never +// follows a planted link even on the read surface. func (sfs *worktreeFilesystem) validReadPath(p string) error { if p == "" || p == "." || p == "/" { return nil } - return sfs.validPath(p) + if err := sfs.validPath(p); err != nil { + return err + } + return sfs.validNoLeadingSymlink(p) } var errUnsupportedOperation = errors.New("unsupported operation") @@ -232,6 +268,55 @@ func (sfs *worktreeFilesystem) validPath(paths ...string) error { return nil } +// validWritePath validates paths for mutating operations. It layers the +// filesystem-state check validNoLeadingSymlink on top of the string-only +// checks in validPath, so a write can neither name a dangerous path nor +// reach one by traversing an existing symlink. Every mutating method on +// the wrapper funnels through here, so the leading-symlink invariant holds +// for all worktree writers without each call site having to remember it. +func (sfs *worktreeFilesystem) validWritePath(paths ...string) error { + if err := sfs.validPath(paths...); err != nil { + return err + } + return sfs.validNoLeadingSymlink(paths...) +} + +// validNoLeadingSymlink rejects paths whose leading directory components +// resolve through a symlink that already exists on the underlying +// filesystem. validPath guards the path string. This guards the on-disk +// state, so a write or delete cannot reach outside the worktree by +// traversing a symlink that a tree or an earlier step left in place. +// +// This is the fail-closed backstop for the whole class. Callers that want +// upstream's replace-and-continue behaviour (checkout) remove the blocking +// symlink first via clearBlockingSymlinks, so no symlink remains when the +// write reaches the wrapper. Callers that do not get a safe error, +// matching upstream Git refusing rather than following the link. See +// has_symlink_leading_path (symlinks.c) and the check_leading_path guard +// in unlink_entry (entry.c). +func (sfs *worktreeFilesystem) validNoLeadingSymlink(paths ...string) error { + for _, p := range paths { + for dir := filepath.Dir(p); dir != "." && dir != "" && dir != string(filepath.Separator); dir = filepath.Dir(dir) { + fi, err := sfs.Filesystem.Lstat(dir) + if err != nil { + // A missing ancestor is materialised as a real directory, + // so it cannot be a symlink and is safe to skip. Any other + // error (permission, I/O) means we cannot prove the + // component is not a symlink, so fail closed rather than + // let the operation traverse an unverified component. + if os.IsNotExist(err) { + continue + } + return fmt.Errorf("invalid path %q: cannot stat leading component %q: %w", p, dir, err) + } + if fi.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("invalid path %q: leading component %q is a symlink", p, dir) + } + } + } + return nil +} + // validSymlinkName checks the per-component name of a symlink for // dotfile names that attackers can use to trick a checkout into // writing a dangerous symlink. Each path component is compared diff --git a/vendor/github.com/go-git/go-git/v5/worktree_status.go b/vendor/github.com/go-git/go-git/v5/worktree_status.go index ecc3d7ab89..8a28231d17 100644 --- a/vendor/github.com/go-git/go-git/v5/worktree_status.go +++ b/vendor/github.com/go-git/go-git/v5/worktree_status.go @@ -371,7 +371,7 @@ func (w *Worktree) doAdd(path string, ignorePattern []gitignore.Pattern, skipSta } } - path = filepath.Clean(path) + path = filepath.ToSlash(filepath.Clean(path)) if err != nil || !fi.IsDir() { added, h, err = w.doAddFile(idx, s, path, ignorePattern) diff --git a/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/sharesstorageprovider/sharesstorageprovider.go b/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/sharesstorageprovider/sharesstorageprovider.go index 4f95bfdaa4..72fb6b0d71 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/sharesstorageprovider/sharesstorageprovider.go +++ b/vendor/github.com/opencloud-eu/reva/v2/internal/grpc/services/sharesstorageprovider/sharesstorageprovider.go @@ -955,6 +955,7 @@ func (s *service) RestoreFileVersion(ctx context.Context, req *provider.RestoreF return gatewayClient.RestoreFileVersion(ctx, &provider.RestoreFileVersionRequest{ Opaque: req.Opaque, Ref: buildReferenceInShare(req.Ref, receivedShare), + Key: req.Key, }) } diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/jsoncs3.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/jsoncs3.go index 925491cdf7..5ba4b5d432 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/jsoncs3.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3/jsoncs3.go @@ -553,16 +553,29 @@ func (m *Manager) GetShare(ctx context.Context, ref *collaboration.ShareReferenc sublog.Error().Err(err). Msg("failed to unshare expired share") } - if err := events.Publish(ctx, m.eventStream, events.ShareExpired{ - ShareID: s.GetId(), - ShareOwner: s.GetOwner(), - ItemID: s.GetResourceId(), - ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())), - GranteeUserID: s.GetGrantee().GetUserId(), - GranteeGroupID: s.GetGrantee().GetGroupId(), - }); err != nil { - sublog.Error().Err(err). - Msg("failed to publish share expired event") + if s.ResourceId.SpaceId == s.ResourceId.OpaqueId { + if err := events.Publish(ctx, m.eventStream, events.SpaceMembershipExpired{ + SpaceOwner: s.GetOwner(), + SpaceID: &provider.StorageSpaceId{OpaqueId: s.ResourceId.StorageId + "$" + s.ResourceId.SpaceId}, + ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())), + GranteeUserID: s.GetGrantee().GetUserId(), + GranteeGroupID: s.GetGrantee().GetGroupId(), + }); err != nil { + sublog.Error().Err(err). + Msg("failed to publish space membership expired event") + } + } else { + if err := events.Publish(ctx, m.eventStream, events.ShareExpired{ + ShareID: s.GetId(), + ShareOwner: s.GetOwner(), + ItemID: s.GetResourceId(), + ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())), + GranteeUserID: s.GetGrantee().GetUserId(), + GranteeGroupID: s.GetGrantee().GetGroupId(), + }); err != nil { + sublog.Error().Err(err). + Msg("failed to publish share expired event") + } } } // check if we are the creator or the grantee @@ -754,15 +767,28 @@ func (m *Manager) listSharesByIDs(ctx context.Context, user *userv1beta1.User, f sublog.Error().Err(err). Msg("failed to unshare expired share") } - if err := events.Publish(ctx, m.eventStream, events.ShareExpired{ - ShareOwner: s.GetOwner(), - ItemID: resourceID, - ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())), - GranteeUserID: s.GetGrantee().GetUserId(), - GranteeGroupID: s.GetGrantee().GetGroupId(), - }); err != nil { - sublog.Error().Err(err). - Msg("failed to publish share expired event") + if s.ResourceId.SpaceId == s.ResourceId.OpaqueId { + if err := events.Publish(ctx, m.eventStream, events.SpaceMembershipExpired{ + SpaceOwner: s.GetOwner(), + SpaceID: &provider.StorageSpaceId{OpaqueId: s.ResourceId.StorageId + "$" + s.ResourceId.SpaceId}, + ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())), + GranteeUserID: s.GetGrantee().GetUserId(), + GranteeGroupID: s.GetGrantee().GetGroupId(), + }); err != nil { + sublog.Error().Err(err). + Msg("failed to publish space membership expired event") + } + } else { + if err := events.Publish(ctx, m.eventStream, events.ShareExpired{ + ShareOwner: s.GetOwner(), + ItemID: resourceID, + ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())), + GranteeUserID: s.GetGrantee().GetUserId(), + GranteeGroupID: s.GetGrantee().GetGroupId(), + }); err != nil { + sublog.Error().Err(err). + Msg("failed to publish share expired event") + } } continue } @@ -872,15 +898,28 @@ func (m *Manager) listCreatedShares(ctx context.Context, user *userv1beta1.User, sublog.Error().Err(err). Msg("failed to unshare expired share") } - if err := events.Publish(ctx, m.eventStream, events.ShareExpired{ - ShareOwner: s.GetOwner(), - ItemID: s.GetResourceId(), - ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())), - GranteeUserID: s.GetGrantee().GetUserId(), - GranteeGroupID: s.GetGrantee().GetGroupId(), - }); err != nil { - sublog.Error().Err(err). - Msg("failed to publish share expired event") + if s.ResourceId.SpaceId == s.ResourceId.OpaqueId { + if err := events.Publish(ctx, m.eventStream, events.SpaceMembershipExpired{ + SpaceOwner: s.GetOwner(), + SpaceID: &provider.StorageSpaceId{OpaqueId: s.ResourceId.StorageId + "$" + s.ResourceId.SpaceId}, + ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())), + GranteeUserID: s.GetGrantee().GetUserId(), + GranteeGroupID: s.GetGrantee().GetGroupId(), + }); err != nil { + sublog.Error().Err(err). + Msg("failed to publish space membership expired event") + } + } else { + if err := events.Publish(ctx, m.eventStream, events.ShareExpired{ + ShareOwner: s.GetOwner(), + ItemID: s.GetResourceId(), + ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())), + GranteeUserID: s.GetGrantee().GetUserId(), + GranteeGroupID: s.GetGrantee().GetGroupId(), + }); err != nil { + sublog.Error().Err(err). + Msg("failed to publish share expired event") + } } continue } @@ -1044,15 +1083,28 @@ func (m *Manager) ListReceivedShares(ctx context.Context, filters []*collaborati sublogr.Error().Err(err). Msg("failed to unshare expired share") } - if err := events.Publish(ctx, m.eventStream, events.ShareExpired{ - ShareOwner: s.GetOwner(), - ItemID: s.GetResourceId(), - ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())), - GranteeUserID: s.GetGrantee().GetUserId(), - GranteeGroupID: s.GetGrantee().GetGroupId(), - }); err != nil { - sublogr.Error().Err(err). - Msg("failed to publish share expired event") + if s.ResourceId.SpaceId == s.ResourceId.OpaqueId { + if err := events.Publish(ctx, m.eventStream, events.SpaceMembershipExpired{ + SpaceOwner: s.GetOwner(), + SpaceID: &provider.StorageSpaceId{OpaqueId: s.ResourceId.StorageId + "$" + s.ResourceId.SpaceId}, + ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())), + GranteeUserID: s.GetGrantee().GetUserId(), + GranteeGroupID: s.GetGrantee().GetGroupId(), + }); err != nil { + sublogr.Error().Err(err). + Msg("failed to publish space membership expired event") + } + } else { + if err := events.Publish(ctx, m.eventStream, events.ShareExpired{ + ShareOwner: s.GetOwner(), + ItemID: s.GetResourceId(), + ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())), + GranteeUserID: s.GetGrantee().GetUserId(), + GranteeGroupID: s.GetGrantee().GetGroupId(), + }); err != nil { + sublogr.Error().Err(err). + Msg("failed to publish share expired event") + } } continue } @@ -1147,15 +1199,28 @@ func (m *Manager) getReceived(ctx context.Context, ref *collaboration.ShareRefer sublog.Error().Err(err). Msg("failed to unshare expired share") } - if err := events.Publish(ctx, m.eventStream, events.ShareExpired{ - ShareOwner: s.GetOwner(), - ItemID: s.GetResourceId(), - ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())), - GranteeUserID: s.GetGrantee().GetUserId(), - GranteeGroupID: s.GetGrantee().GetGroupId(), - }); err != nil { - sublog.Error().Err(err). - Msg("failed to publish share expired event") + if s.ResourceId.SpaceId == s.ResourceId.OpaqueId { + if err := events.Publish(ctx, m.eventStream, events.SpaceMembershipExpired{ + SpaceOwner: s.GetOwner(), + SpaceID: &provider.StorageSpaceId{OpaqueId: s.ResourceId.StorageId + "$" + s.ResourceId.SpaceId}, + ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())), + GranteeUserID: s.GetGrantee().GetUserId(), + GranteeGroupID: s.GetGrantee().GetGroupId(), + }); err != nil { + sublog.Error().Err(err). + Msg("failed to publish space membership expired event") + } + } else { + if err := events.Publish(ctx, m.eventStream, events.ShareExpired{ + ShareOwner: s.GetOwner(), + ItemID: s.GetResourceId(), + ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())), + GranteeUserID: s.GetGrantee().GetUserId(), + GranteeGroupID: s.GetGrantee().GetGroupId(), + }); err != nil { + sublog.Error().Err(err). + Msg("failed to publish share expired event") + } } } return m.convert(ctx, user.Id.GetOpaqueId(), s), nil diff --git a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/utils/decomposedfs/spaces.go b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/utils/decomposedfs/spaces.go index ad40ab37af..83bbec2732 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/utils/decomposedfs/spaces.go +++ b/vendor/github.com/opencloud-eu/reva/v2/pkg/storage/utils/decomposedfs/spaces.go @@ -39,6 +39,7 @@ import ( ocsconv "github.com/opencloud-eu/reva/v2/pkg/conversions" ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx" "github.com/opencloud-eu/reva/v2/pkg/errtypes" + "github.com/opencloud-eu/reva/v2/pkg/events" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" sdk "github.com/opencloud-eu/reva/v2/pkg/sdk/common" @@ -847,20 +848,10 @@ func (fs *Decomposedfs) linkStorageSpaceType(ctx context.Context, spaceType, spa func (fs *Decomposedfs) StorageSpaceFromNode(ctx context.Context, n *node.Node, checkPermissions bool) (*provider.StorageSpace, error) { user := ctxpkg.ContextMustGetUser(ctx) - if checkPermissions { + if checkPermissions && n.SpaceRoot.IsDisabled(ctx) { rp, err := fs.p.AssemblePermissions(ctx, n) - switch { - case err != nil: - return nil, err - case !rp.Stat: - return nil, errtypes.NotFound(fmt.Sprintf("space %s not found", n.ID)) - } - - if n.SpaceRoot.IsDisabled(ctx) { - rp, err := fs.p.AssemblePermissions(ctx, n) - if err != nil || !permissions.IsManager(rp) { - return nil, errtypes.PermissionDenied(fmt.Sprintf("user %s is not allowed to list deleted spaces %s", user.Username, n.ID)) - } + if err != nil || !permissions.IsManager(rp) { + return nil, errtypes.PermissionDenied(fmt.Sprintf("user %s is not allowed to list deleted spaces %s", user.Username, n.ID)) } } @@ -906,7 +897,10 @@ func (fs *Decomposedfs) StorageSpaceFromNode(ctx context.Context, n *node.Node, // This way we don't have to have a cron job checking the grants in regular intervals. // The tradeof obviously is that this code is here. if isGrantExpired(g) { - if err := n.DeleteGrant(ctx, g, true); err != nil { + var errDeleteGrant, errIndexRemove error + + errDeleteGrant = n.DeleteGrant(ctx, g, true) + if errDeleteGrant != nil { sublog.Error().Err(err).Str("grantee", id). Msg("failed to delete expired space grant") } @@ -915,19 +909,43 @@ func (fs *Decomposedfs) StorageSpaceFromNode(ctx context.Context, n *node.Node, switch g.Grantee.Type { case provider.GranteeType_GRANTEE_TYPE_USER: // remove from user index - if err := fs.userSpaceIndex.Remove(g.Grantee.GetUserId().GetOpaqueId(), n.SpaceID); err != nil { + errIndexRemove = fs.userSpaceIndex.Remove(g.Grantee.GetUserId().GetOpaqueId(), n.SpaceID) + if errIndexRemove != nil { sublog.Error().Err(err).Str("grantee", id). Msg("failed to delete expired user space index") } case provider.GranteeType_GRANTEE_TYPE_GROUP: // remove from group index - if err := fs.groupSpaceIndex.Remove(g.Grantee.GetGroupId().GetOpaqueId(), n.SpaceID); err != nil { + errIndexRemove = fs.groupSpaceIndex.Remove(g.Grantee.GetGroupId().GetOpaqueId(), n.SpaceID) + if errIndexRemove != nil { sublog.Error().Err(err).Str("grantee", id). Msg("failed to delete expired group space index") } } } + // publish SpaceMembershipExpired event + if errDeleteGrant == nil { + ev := events.SpaceMembershipExpired{ + SpaceOwner: n.SpaceOwnerOrManager(ctx), + SpaceID: &provider.StorageSpaceId{OpaqueId: n.SpaceID}, + SpaceName: sname, + ExpiredAt: time.Unix(int64(g.Expiration.Seconds), int64(g.Expiration.Nanos)), + Timestamp: utils.TSNow(), + } + switch g.Grantee.Type { + case provider.GranteeType_GRANTEE_TYPE_USER: + ev.GranteeUserID = g.Grantee.GetUserId() + case provider.GranteeType_GRANTEE_TYPE_GROUP: + ev.GranteeGroupID = g.Grantee.GetGroupId() + } + if fs.stream != nil { + if err := events.Publish(ctx, fs.stream, ev); err != nil { + sublog.Error().Err(err).Msg("error publishing SpaceMembershipExpired event") + } + } + } + continue } grantExpiration[id] = g.Expiration @@ -935,6 +953,17 @@ func (fs *Decomposedfs) StorageSpaceFromNode(ctx context.Context, n *node.Node, grantMap[id] = g.Permissions } + // check permissions after expired grants have been removed + if checkPermissions { + rp, err := fs.p.AssemblePermissions(ctx, n) + switch { + case err != nil: + return nil, err + case !rp.Stat: + return nil, errtypes.NotFound(fmt.Sprintf("space %s not found", n.ID)) + } + } + grantMapJSON, err := json.Marshal(grantMap) if err != nil { return nil, err diff --git a/vendor/modules.txt b/vendor/modules.txt index 5ec9ecb45a..0e662a82df 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -475,7 +475,7 @@ github.com/go-git/go-billy/v5/helper/polyfill github.com/go-git/go-billy/v5/memfs github.com/go-git/go-billy/v5/osfs github.com/go-git/go-billy/v5/util -# github.com/go-git/go-git/v5 v5.19.1 +# github.com/go-git/go-git/v5 v5.19.2 ## explicit; go 1.25.0 github.com/go-git/go-git/v5 github.com/go-git/go-git/v5/config @@ -1364,7 +1364,7 @@ github.com/opencloud-eu/icap-client # github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d ## explicit; go 1.18 github.com/opencloud-eu/libre-graph-api-go -# github.com/opencloud-eu/reva/v2 v2.48.0 +# github.com/opencloud-eu/reva/v2 v2.48.1-0.20260812174127-12b756ed83c5 ## explicit; go 1.25.8 github.com/opencloud-eu/reva/v2/cmd/revad/internal/grace github.com/opencloud-eu/reva/v2/cmd/revad/runtime @@ -1862,7 +1862,7 @@ github.com/rcrowley/go-metrics ## explicit; go 1.22.0 github.com/riandyrn/otelchi github.com/riandyrn/otelchi/version -# github.com/rogpeppe/go-internal v1.15.0 +# github.com/rogpeppe/go-internal v1.16.0 ## explicit; go 1.25 github.com/rogpeppe/go-internal/internal/syscall/windows github.com/rogpeppe/go-internal/internal/syscall/windows/sysdll From 42e7ab55b6cef9f6805c7d72cea61b0484132992 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Wed, 12 Aug 2026 21:53:32 +0200 Subject: [PATCH 4/8] send email when a share is removed (backport https://github.com/owncloud/ocis/pull/10915) --- services/notifications/pkg/command/server.go | 1 + services/notifications/pkg/email/templates.go | 13 ++++ services/notifications/pkg/service/job.go | 19 +++++ services/notifications/pkg/service/service.go | 2 + services/notifications/pkg/service/shares.go | 70 +++++++++++++++++++ .../settings/pkg/store/defaults/defaults.go | 2 +- 6 files changed, 106 insertions(+), 1 deletion(-) diff --git a/services/notifications/pkg/command/server.go b/services/notifications/pkg/command/server.go index c8f9b2e792..f1fbfa4d78 100644 --- a/services/notifications/pkg/command/server.go +++ b/services/notifications/pkg/command/server.go @@ -84,6 +84,7 @@ func Server(cfg *config.Config) *cobra.Command { evs := []events.Unmarshaller{ events.ShareCreated{}, events.ShareExpired{}, + events.ShareRemoved{}, events.SpaceShared{}, events.SpaceUnshared{}, events.SpaceMembershipExpired{}, diff --git a/services/notifications/pkg/email/templates.go b/services/notifications/pkg/email/templates.go index 99091b44b1..5ab5d813c1 100644 --- a/services/notifications/pkg/email/templates.go +++ b/services/notifications/pkg/email/templates.go @@ -23,6 +23,19 @@ var ( CallToAction: l10n.Template(`Click here to view it: {ShareLink}`), } + ShareRemoved = MessageTemplate{ + textTemplate: _textTemplate, + htmlTemplate: _htmlTemplate, + // ShareRemoved email template, Subject field (resolves directly) + Subject: l10n.Template(`{ShareSharer} unshared '{ShareFolder}' with you`), + // ShareRemoved email template, resolves via {{ .Greeting }} + Greeting: l10n.Template(`Hello {ShareGrantee},`), + // ShareRemoved email template, resolves via {{ .MessageBody }} + MessageBody: l10n.Template(`{ShareSharer} has unshared '{ShareFolder}' with you. + +Even though this share has been revoked you still might have access through other shares and/or space memberships.`), + } + ShareExpired = MessageTemplate{ textTemplate: _textTemplate, htmlTemplate: _htmlTemplate, diff --git a/services/notifications/pkg/service/job.go b/services/notifications/pkg/service/job.go index e5a39126cf..96884865e6 100644 --- a/services/notifications/pkg/service/job.go +++ b/services/notifications/pkg/service/job.go @@ -130,6 +130,25 @@ func (s eventsNotifier) createGroupedMail(ctx context.Context, logger zerolog.Lo "ShareFolder": shareFolder, "ExpiredAt": te.ExpiredAt.Format("2006-01-02 15:04:05"), }) + case events.ShareRemoved: + logger := logger.With(). + Str("event", "ShareRemoved"). + Str("eventId", te.ItemID.OpaqueId). + Logger() + + executant, shareFolder, _, err := s.prepareShareRemoved(logger, te) + if err != nil { + logger.Error().Err(err).Msg("could not prepare vars for grouped email") + continue + } + + mts = append(mts, email.ShareRemoved) + mtsVars = append(mtsVars, map[string]string{ + "ShareSharer": executant.GetDisplayName(), + "ShareFolder": shareFolder, + }) + default: + logger.Error().Str("eventType", e.Type).Msg("unsupported event type for grouped email") } } diff --git a/services/notifications/pkg/service/service.go b/services/notifications/pkg/service/service.go index 65707bb355..74b2e491e8 100644 --- a/services/notifications/pkg/service/service.go +++ b/services/notifications/pkg/service/service.go @@ -129,6 +129,8 @@ EventLoop: s.handleShareCreated(e, evt.ID) case events.ShareExpired: s.handleShareExpired(e, evt.ID) + case events.ShareRemoved: + s.handleShareRemoved(e, evt.ID) case events.ScienceMeshInviteTokenGenerated: s.handleScienceMeshInviteTokenGenerated(e) case events.SendEmailsEvent: diff --git a/services/notifications/pkg/service/shares.go b/services/notifications/pkg/service/shares.go index 01d4825ab3..aa905b630a 100644 --- a/services/notifications/pkg/service/shares.go +++ b/services/notifications/pkg/service/shares.go @@ -161,3 +161,73 @@ func (s eventsNotifier) prepareShareExpired(logger zerolog.Logger, e events.Shar return shareFolder, ctx, err } + +func (s eventsNotifier) handleShareRemoved(e events.ShareRemoved, eventId string) { + logger := s.logger.With(). + Str("event", "ShareRemoved"). + Str("itemid", e.ItemID.OpaqueId). + Logger() + + executant, shareFolder, ctx, err := s.prepareShareRemoved(logger, e) + if err != nil { + logger.Error().Err(err).Msg("could not prepare vars for email") + return + } + + granteeList := s.ensureGranteeList(ctx, executant.GetId(), e.GranteeUserID, e.GranteeGroupID) + filteredGrantees := s.filter.execute(ctx, granteeList, defaults.SettingUUIDProfileEventShareRemoved) + + recipientsInstant, recipientsDaily, recipientsInstantWeekly := s.splitter.execute(ctx, filteredGrantees) + recipientsInstant = append(recipientsInstant, s.userEventStore.persist(_intervalDaily, eventId, recipientsDaily)...) + recipientsInstant = append(recipientsInstant, s.userEventStore.persist(_intervalWeekly, eventId, recipientsInstantWeekly)...) + if recipientsInstant == nil { + return + } + + sharerDisplayName := executant.GetDisplayName() + + emails, err := s.render(ctx, email.ShareRemoved, + "ShareGrantee", + map[string]string{ + "ShareSharer": sharerDisplayName, + "ShareFolder": shareFolder, + }, recipientsInstant, sharerDisplayName) + if err != nil { + logger.Error().Err(err).Msg("could not get render the email") + return + } + s.send(ctx, emails) +} + +func (s eventsNotifier) prepareShareRemoved(logger zerolog.Logger, e events.ShareRemoved) (executant *user.User, shareFolder string, ctx context.Context, err error) { + gatewayClient, err := s.gatewaySelector.Next() + if err != nil { + logger.Error().Err(err).Msg("could not select next gateway client") + return executant, shareFolder, ctx, err + } + + ctx, err = utils.GetServiceUserContextWithContext(context.Background(), gatewayClient, s.serviceAccountID, s.serviceAccountSecret) + if err != nil { + logger.Error().Err(err).Msg("could not get service user context") + return executant, shareFolder, ctx, err + } + + resourceInfo, err := s.getResourceInfo(ctx, e.ItemID, &fieldmaskpb.FieldMask{Paths: []string{"name"}}) + if err != nil { + logger.Error(). + Err(err). + Msg("could not stat resource") + return executant, shareFolder, ctx, err + } + shareFolder = resourceInfo.Name + + executant, err = utils.GetUserNoGroups(ctx, e.Executant, gatewayClient) + if err != nil { + logger.Error(). + Err(err). + Msg("could not get user") + return executant, shareFolder, ctx, err + } + + return executant, shareFolder, ctx, err +} diff --git a/services/settings/pkg/store/defaults/defaults.go b/services/settings/pkg/store/defaults/defaults.go index c8d144eefd..5f3d9e0fa1 100644 --- a/services/settings/pkg/store/defaults/defaults.go +++ b/services/settings/pkg/store/defaults/defaults.go @@ -331,7 +331,7 @@ func generateBundleProfileRequest() *settingsmsg.Bundle { MultiChoiceCollectionValue: &settingsmsg.MultiChoiceCollection{ Options: []*settingsmsg.MultiChoiceCollectionOption{ &optionInAppTrue, - &optionMailFalseDisabled, + &optionMailTrue, }, }, }, From 75e9319065d029cc13c58b69788ea6e81489abfa Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Wed, 12 Aug 2026 21:54:52 +0200 Subject: [PATCH 5/8] SpaceMembershipExpired notification email fix --- .../notifications/pkg/service/service_test.go | 42 ++++++++++++-- services/notifications/pkg/service/spaces.go | 58 ++++++++++++------- 2 files changed, 73 insertions(+), 27 deletions(-) diff --git a/services/notifications/pkg/service/service_test.go b/services/notifications/pkg/service/service_test.go index e00b04cf11..0605b9ba76 100644 --- a/services/notifications/pkg/service/service_test.go +++ b/services/notifications/pkg/service/service_test.go @@ -65,8 +65,12 @@ var _ = Describe("Notifications", func() { }, ) - gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil).Once() - gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharee}, nil).Once() + gatewayClient.On("GetUser", mock.Anything, mock.MatchedBy(func(req *user.GetUserRequest) bool { + return req.GetUserId().GetOpaqueId() == sharer.GetId().GetOpaqueId() + })).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil) + gatewayClient.On("GetUser", mock.Anything, mock.MatchedBy(func(req *user.GetUserRequest) bool { + return req.GetUserId().GetOpaqueId() == sharee.GetId().GetOpaqueId() + })).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharee}, nil) gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil) gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, Info: &provider.ResourceInfo{Name: "secrets of the board", Space: &provider.StorageSpace{Name: "secret space"}}}, nil) vs = &settingsmocks.ValueService{} @@ -157,6 +161,30 @@ https://opencloud.eu }, }), + Entry("Share Removed", testChannel{ + expectedReceipients: []string{sharee.GetMail()}, + expectedSubject: "Dr. S. Harer unshared 'secrets of the board' with you", + expectedTextBody: `Hello Eric Expireling, + +Dr. S. Harer has unshared 'secrets of the board' with you. + +Even though this share has been revoked you still might have access through other shares and/or space memberships. + + +--- +OpenCloud - a safe home for all your data +https://opencloud.eu +`, + expectedSender: sharer.GetDisplayName(), + done: make(chan struct{}), + }, events.Event{ + Event: events.ShareRemoved{ + Executant: sharer.GetId(), + GranteeUserID: sharee.GetId(), + ItemID: resourceid, + }, + }), + Entry("Added to Space", testChannel{ expectedReceipients: []string{sharee.GetMail()}, expectedSubject: "Dr. S. Harer invited you to join secret space", @@ -222,7 +250,7 @@ Even though this membership has expired you still might have access through othe OpenCloud - a safe home for all your data https://opencloud.eu `, - expectedSender: sharer.GetDisplayName(), + expectedSender: "", done: make(chan struct{}), }, events.Event{ Event: events.SpaceMembershipExpired{ @@ -273,8 +301,12 @@ var _ = Describe("Notifications X-Site Scripting", func() { }, ) - gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil).Once() - gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharee}, nil).Once() + gatewayClient.On("GetUser", mock.Anything, mock.MatchedBy(func(req *user.GetUserRequest) bool { + return req.GetUserId().GetOpaqueId() == sharer.GetId().GetOpaqueId() + })).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil) + gatewayClient.On("GetUser", mock.Anything, mock.MatchedBy(func(req *user.GetUserRequest) bool { + return req.GetUserId().GetOpaqueId() == sharee.GetId().GetOpaqueId() + })).Return(&user.GetUserResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharee}, nil) gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, User: sharer}, nil) gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{ Status: &rpc.Status{Code: rpc.Code_CODE_OK}, diff --git a/services/notifications/pkg/service/spaces.go b/services/notifications/pkg/service/spaces.go index fa987f1b21..1cb30782ee 100644 --- a/services/notifications/pkg/service/spaces.go +++ b/services/notifications/pkg/service/spaces.go @@ -187,30 +187,13 @@ func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExp Str("itemid", e.SpaceID.GetOpaqueId()). Logger() - gatewayClient, err := s.gatewaySelector.Next() - if err != nil { - logger.Error().Err(err).Msg("could not select next gateway client") - return - } - - ctx, err := utils.GetServiceUserContext(s.serviceAccountID, gatewayClient, s.serviceAccountSecret) - if err != nil { - logger.Error().Err(err).Msg("Could not impersonate sharer") - return - } - - owner, err := utils.GetUserNoGroups(ctx, e.SpaceOwner, gatewayClient) + spaceName, ctx, err := s.prepareSpaceMembershipExpired(logger, e) if err != nil { - logger.Error(). - Err(err). - Msg("could not get user") + logger.Error().Err(err).Msg("could not prepare vars for email") return } - granteeList := s.ensureGranteeList(ctx, owner.GetId(), e.GranteeUserID, e.GranteeGroupID) - if granteeList == nil { - return - } + granteeList := s.ensureGranteeList(ctx, nil, e.GranteeUserID, e.GranteeGroupID) filteredGrantees := s.filter.execute(ctx, granteeList, defaults.SettingUUIDProfileEventSpaceMembershipExpired) recipientsInstant, recipientsDaily, recipientsInstantWeekly := s.splitter.execute(ctx, filteredGrantees) @@ -223,12 +206,43 @@ func (s eventsNotifier) handleSpaceMembershipExpired(e events.SpaceMembershipExp emails, err := s.render(ctx, email.MembershipExpired, "SpaceGrantee", map[string]string{ - "SpaceName": e.SpaceName, + "SpaceName": spaceName, "ExpiredAt": e.ExpiredAt.Format("2006-01-02 15:04:05"), - }, recipientsInstant, owner.GetDisplayName()) + }, recipientsInstant, s.defaultEmailSender) if err != nil { logger.Error().Err(err).Msg("could not get render the email") return } s.send(ctx, emails) } + +func (s eventsNotifier) prepareSpaceMembershipExpired(logger zerolog.Logger, e events.SpaceMembershipExpired) (spaceName string, ctx context.Context, err error) { + gatewayClient, err := s.gatewaySelector.Next() + if err != nil { + logger.Error().Err(err).Msg("could not select next gateway client") + return spaceName, ctx, err + } + + ctx, err = utils.GetServiceUserContextWithContext(context.Background(), gatewayClient, s.serviceAccountID, s.serviceAccountSecret) + if err != nil { + logger.Error().Err(err).Msg("could not get service user context") + return spaceName, ctx, err + } + + resourceID, err := storagespace.ParseID(e.SpaceID.GetOpaqueId()) + if err != nil { + logger.Error().Err(err).Msg("could not parse SpaceID") + return spaceName, ctx, err + } + + resourceInfo, err := s.getResourceInfo(ctx, &resourceID, nil) + if err != nil { + logger.Error(). + Err(err). + Msg("could not get space info") + return spaceName, ctx, err + } + spaceName = resourceInfo.GetSpace().GetName() + + return spaceName, ctx, err +} From 848583e639ccede2d87a577a5ab74958427a3967 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Wed, 12 Aug 2026 22:28:03 +0200 Subject: [PATCH 6/8] fix in app space memebership expired notification --- services/userlog/pkg/service/conversion.go | 36 +++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/services/userlog/pkg/service/conversion.go b/services/userlog/pkg/service/conversion.go index 5a6547ee91..621d94c742 100644 --- a/services/userlog/pkg/service/conversion.go +++ b/services/userlog/pkg/service/conversion.go @@ -108,7 +108,7 @@ func (c *Converter) ConvertEvent(eventid string, event any) (OC10Notification, e case events.SpaceUnshared: return c.spaceMessage(eventid, SpaceUnshared, ev.Executant, ev.ID.GetOpaqueId(), ev.Timestamp) case events.SpaceMembershipExpired: - return c.spaceMessage(eventid, SpaceMembershipExpired, ev.SpaceOwner, ev.SpaceID.GetOpaqueId(), ev.ExpiredAt) + return c.spaceMembershipExpiredMessage(eventid, SpaceMembershipExpired, ev.SpaceID.GetOpaqueId(), ev.ExpiredAt) // share related case events.ShareCreated: @@ -206,6 +206,40 @@ func (c *Converter) spaceMessage(eventid string, nt NotificationTemplate, execut }, nil } +func (c *Converter) spaceMembershipExpiredMessage(eventid string, nt NotificationTemplate, spaceid string, ts time.Time) (OC10Notification, error) { + space, err := c.getSpace(c.serviceAccountContext, spaceid) + if err != nil { + return OC10Notification{}, err + } + + subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.defaultLanguage, c.translationPath, map[string]any{ + "spacename": space.GetName(), + }) + if err != nil { + return OC10Notification{}, err + } + + dets := map[string]any{ + "space": map[string]string{ + "id": space.GetId().GetOpaqueId(), + "name": space.GetName(), + }, + } + + return OC10Notification{ + EventID: eventid, + Service: c.serviceName, + Timestamp: ts.Format(time.RFC3339Nano), + ResourceID: spaceid, + ResourceType: _resourceTypeSpace, + Subject: subj, + SubjectRaw: subjraw, + Message: msg, + MessageRaw: msgraw, + MessageDetails: dets, + }, nil +} + func (c *Converter) resourceMention(eventid string, nt NotificationTemplate, executant *user.UserId, resourceid *storageprovider.ResourceId, ts time.Time) (OC10Notification, error) { usr, err := c.getUser(context.Background(), executant) if err != nil { From efbfb4c20db8524ffa1c3c015976a4813f2dac35 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Thu, 13 Aug 2026 09:27:13 +0200 Subject: [PATCH 7/8] upd acceptance tests --- tests/acceptance/expected-failures-decomposed-storage.md | 4 ---- tests/acceptance/expected-failures-posix-storage.md | 4 ---- 2 files changed, 8 deletions(-) diff --git a/tests/acceptance/expected-failures-decomposed-storage.md b/tests/acceptance/expected-failures-decomposed-storage.md index 5e93d5f941..114c44e797 100644 --- a/tests/acceptance/expected-failures-decomposed-storage.md +++ b/tests/acceptance/expected-failures-decomposed-storage.md @@ -345,10 +345,6 @@ _ocdav: api compatibility, return correct status code_ - [cliCommands/restoreTrashBinItems.feature:23](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/cliCommands/restoreTrashBinItems.feature#L23) - [cliCommands/sharesCleanup.feature:12](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/cliCommands/sharesCleanup.feature#L12) -#### [user with roleFileEditorWithVersions permission cannot restore version of file](https://github.com/opencloud-eu/opencloud/issues/3168) - -- [apiSharingNg1/shareFileVersions.feature:55](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSharingNg1/shareFileVersions.feature#L55) -- [apiSharingNg1/shareFileVersions.feature:56](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSharingNg1/shareFileVersions.feature#L56) ### Won't fix diff --git a/tests/acceptance/expected-failures-posix-storage.md b/tests/acceptance/expected-failures-posix-storage.md index 035b126b91..8aa87f1d07 100644 --- a/tests/acceptance/expected-failures-posix-storage.md +++ b/tests/acceptance/expected-failures-posix-storage.md @@ -350,10 +350,6 @@ tests/acceptance/features/cliCommands/restoreTrashBinItems.feature - [cliCommands/restoreTrashBinItems.feature:23](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/cliCommands/restoreTrashBinItems.feature#L23) - [cliCommands/sharesCleanup.feature:12](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/cliCommands/sharesCleanup.feature#L12) -#### [user with roleFileEditorWithVersions permission cannot restore version of file](https://github.com/opencloud-eu/opencloud/issues/3168) - -- [apiSharingNg1/shareFileVersions.feature:55](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSharingNg1/shareFileVersions.feature#L55) -- [apiSharingNg1/shareFileVersions.feature:56](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiSharingNg1/shareFileVersions.feature#L56) ### Won't fix From 1cbc833f8d7daa7006d8aedefd965e95abe18bf4 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Thu, 13 Aug 2026 14:27:57 +0200 Subject: [PATCH 8/8] upd test cases after intorduced fixes --- tests/acceptance/expected-failures-decomposed-storage.md | 4 ---- tests/acceptance/expected-failures-posix-storage.md | 4 ---- .../features/apiNotification/emailNotification.feature | 6 ++++-- .../features/apiNotification/spaceNotification.feature | 1 + 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/acceptance/expected-failures-decomposed-storage.md b/tests/acceptance/expected-failures-decomposed-storage.md index e107e3e279..6d92126480 100644 --- a/tests/acceptance/expected-failures-decomposed-storage.md +++ b/tests/acceptance/expected-failures-decomposed-storage.md @@ -346,10 +346,6 @@ _ocdav: api compatibility, return correct status code_ - [cliCommands/sharesCleanup.feature:12](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/cliCommands/sharesCleanup.feature#L12) -### notification issue #323 -- [apiNotification/emailNotification.feature:280](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiNotification/emailNotification.feature#L280) -- [apiNotification/emailNotification.feature:298](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiNotification/emailNotification.feature#L298) -- [apiNotification/spaceNotification.feature:463](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiNotification/spaceNotification.feature#L463) ### Won't fix diff --git a/tests/acceptance/expected-failures-posix-storage.md b/tests/acceptance/expected-failures-posix-storage.md index 530b0b662b..013a6528ce 100644 --- a/tests/acceptance/expected-failures-posix-storage.md +++ b/tests/acceptance/expected-failures-posix-storage.md @@ -351,10 +351,6 @@ tests/acceptance/features/cliCommands/restoreTrashBinItems.feature - [cliCommands/sharesCleanup.feature:12](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/cliCommands/sharesCleanup.feature#L12) -### notification issue #323 -- [apiNotification/emailNotification.feature:280](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiNotification/emailNotification.feature#L280) -- [apiNotification/emailNotification.feature:298](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiNotification/emailNotification.feature#L298) -- [apiNotification/spaceNotification.feature:463](https://github.com/opencloud-eu/opencloud/blob/main/tests/acceptance/features/apiNotification/spaceNotification.feature#L463) ### Won't fix diff --git a/tests/acceptance/features/apiNotification/emailNotification.feature b/tests/acceptance/features/apiNotification/emailNotification.feature index b08d2f8a9e..56e77944f7 100644 --- a/tests/acceptance/features/apiNotification/emailNotification.feature +++ b/tests/acceptance/features/apiNotification/emailNotification.feature @@ -306,9 +306,11 @@ Feature: Email notification When user "Alice" has removed the access of user "Brian" from resource "lorem.txt" of space "Personal" Then user "Brian" should have received the following email from user "Alice" """ - Hello Brian Murphy + Hello Brian Murphy, + + %displayname% has unshared 'lorem.txt' with you. - %displayname% has unshared "lorem.txt" with you. + Even though this share has been revoked you still might have access through other shares and/or space memberships. """ diff --git a/tests/acceptance/features/apiNotification/spaceNotification.feature b/tests/acceptance/features/apiNotification/spaceNotification.feature index 4ad280a05a..717d2ed3d1 100644 --- a/tests/acceptance/features/apiNotification/spaceNotification.feature +++ b/tests/acceptance/features/apiNotification/spaceNotification.feature @@ -464,6 +464,7 @@ Feature: Notification When user "Alice" expires the user share of space "notification checking" for user "Brian" Then the HTTP status code should be "200" # trigger the notification to be sent to the user + When user "Brian" lists the shares shared with him using the Graph API And user "Brian" should get a notification with subject "Membership expired" and message: | message | | Access to Space notification checking lost |