Skip to content

Commit b78e58c

Browse files
committed
feat: [OCISDEV-1437] show suggested expiry date of 30 days
1 parent f722457 commit b78e58c

5 files changed

Lines changed: 173 additions & 5 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Change: Default 30-day expiration for personalized shares
2+
3+
When a user or group share of a file or folder is created without an
4+
expiration date, the server now preconfigures a default expiration of 30 days
5+
to encourage data minimization. The sharer can override or shorten this value.
6+
7+
Space memberships are exempt and stay unbounded, as they represent a permanent
8+
organizational role.
9+
10+
https://github.com/owncloud/ocis/pull/12988

‎services/graph/pkg/service/v0/api_driveitem_permissions.go‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/url"
88
"slices"
99
"strings"
10+
"time"
1011

1112
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
1213
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
@@ -46,6 +47,9 @@ import (
4647
const (
4748
invalidIdMsg = "invalid driveID or itemID"
4849
parseDriveIDErrMsg = "could not parse driveID"
50+
51+
// default expiration for user/group shares created without one; space memberships are exempt
52+
defaultShareExpirationDays = 30
4953
)
5054

5155
// DriveItemPermissionsProvider contains the methods related to handling permissions on drive items
@@ -177,6 +181,16 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *sto
177181
var shareid string
178182
var expiration *types.Timestamp
179183
var cTime *types.Timestamp
184+
185+
// use the client-supplied expiration, else default non-space shares to defaultShareExpirationDays
186+
var shareExpiration *types.Timestamp
187+
switch {
188+
case invite.ExpirationDateTime != nil:
189+
shareExpiration = utils.TimeToTS(*invite.ExpirationDateTime)
190+
case !IsSpaceRoot(statResponse.GetInfo().GetId()):
191+
shareExpiration = utils.TimeToTS(time.Now().UTC().AddDate(0, 0, defaultShareExpirationDays))
192+
}
193+
180194
switch driveRecipient.GetLibreGraphRecipientType() {
181195
case "group":
182196
group, err := s.identityCache.GetGroup(ctx, objectID)
@@ -192,8 +206,8 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *sto
192206
},
193207
}
194208
createShareRequest := createShareRequestToGroup(group, statResponse.GetInfo(), cs3ResourcePermissions)
195-
if invite.ExpirationDateTime != nil {
196-
createShareRequest.GetGrant().Expiration = utils.TimeToTS(*invite.ExpirationDateTime)
209+
if shareExpiration != nil {
210+
createShareRequest.GetGrant().Expiration = shareExpiration
197211
}
198212
createShareResponse, err := gatewayClient.CreateShare(ctx, createShareRequest)
199213
if err := errorcode.FromCS3Status(createShareResponse.GetStatus(), err); err != nil {
@@ -259,8 +273,8 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *sto
259273
expiration = createShareResponse.GetShare().GetExpiration()
260274
} else {
261275
createShareRequest := createShareRequestToUser(user, statResponse.GetInfo(), cs3ResourcePermissions)
262-
if invite.ExpirationDateTime != nil {
263-
createShareRequest.GetGrant().Expiration = utils.TimeToTS(*invite.ExpirationDateTime)
276+
if shareExpiration != nil {
277+
createShareRequest.GetGrant().Expiration = shareExpiration
264278
}
265279
createShareResponse, err := gatewayClient.CreateShare(ctx, createShareRequest)
266280
if err := errorcode.FromCS3Status(createShareResponse.GetStatus(), err); err != nil {

‎services/graph/pkg/service/v0/api_driveitem_permissions_test.go‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,71 @@ var _ = Describe("DriveItemPermissionsService", func() {
174174
Expect(permission.GrantedToV2.Group.GetId()).To(Equal("2"))
175175
})
176176

177+
It("applies a default 30-day expiration to a user share created without one", func() {
178+
var capturedReq *collaboration.CreateShareRequest
179+
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
180+
gatewayClient.On("CreateShare", mock.Anything, mock.Anything).
181+
Run(func(args mock.Arguments) {
182+
capturedReq = args.Get(1).(*collaboration.CreateShareRequest)
183+
}).Return(createShareResponse, nil)
184+
driveItemInvite.Recipients = []libregraph.DriveRecipient{
185+
{ObjectId: libregraph.PtrString("1"), LibreGraphRecipientType: libregraph.PtrString("user")},
186+
}
187+
driveItemInvite.ExpirationDateTime = nil
188+
createShareResponse.Share = &collaboration.Share{
189+
Id: &collaboration.ShareId{OpaqueId: "123"},
190+
}
191+
192+
expectedDefault := time.Now().UTC().AddDate(0, 0, 30)
193+
_, err := driveItemPermissionsService.Invite(context.Background(), driveItemId, driveItemInvite)
194+
Expect(err).ToNot(HaveOccurred())
195+
Expect(capturedReq.GetGrant().GetExpiration()).ToNot(BeNil())
196+
Expect(utils.TSToTime(capturedReq.GetGrant().GetExpiration())).To(BeTemporally("~", expectedDefault, time.Minute))
197+
})
198+
199+
It("applies a default 30-day expiration to a group share created without one", func() {
200+
var capturedReq *collaboration.CreateShareRequest
201+
gatewayClient.On("GetGroup", mock.Anything, mock.Anything).Return(getGroupResponse, nil)
202+
gatewayClient.On("CreateShare", mock.Anything, mock.Anything).
203+
Run(func(args mock.Arguments) {
204+
capturedReq = args.Get(1).(*collaboration.CreateShareRequest)
205+
}).Return(createShareResponse, nil)
206+
driveItemInvite.Recipients = []libregraph.DriveRecipient{
207+
{ObjectId: libregraph.PtrString("2"), LibreGraphRecipientType: libregraph.PtrString("group")},
208+
}
209+
driveItemInvite.ExpirationDateTime = nil
210+
createShareResponse.Share = &collaboration.Share{
211+
Id: &collaboration.ShareId{OpaqueId: "123"},
212+
}
213+
214+
expectedDefault := time.Now().UTC().AddDate(0, 0, 30)
215+
_, err := driveItemPermissionsService.Invite(context.Background(), driveItemId, driveItemInvite)
216+
Expect(err).ToNot(HaveOccurred())
217+
Expect(capturedReq.GetGrant().GetExpiration()).ToNot(BeNil())
218+
Expect(utils.TSToTime(capturedReq.GetGrant().GetExpiration())).To(BeTemporally("~", expectedDefault, time.Minute))
219+
})
220+
221+
It("keeps a client-provided expiration instead of the default", func() {
222+
var capturedReq *collaboration.CreateShareRequest
223+
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
224+
gatewayClient.On("CreateShare", mock.Anything, mock.Anything).
225+
Run(func(args mock.Arguments) {
226+
capturedReq = args.Get(1).(*collaboration.CreateShareRequest)
227+
}).Return(createShareResponse, nil)
228+
driveItemInvite.Recipients = []libregraph.DriveRecipient{
229+
{ObjectId: libregraph.PtrString("1"), LibreGraphRecipientType: libregraph.PtrString("user")},
230+
}
231+
explicit := time.Now().Add(time.Hour)
232+
driveItemInvite.ExpirationDateTime = libregraph.PtrTime(explicit)
233+
createShareResponse.Share = &collaboration.Share{
234+
Id: &collaboration.ShareId{OpaqueId: "123"},
235+
}
236+
237+
_, err := driveItemPermissionsService.Invite(context.Background(), driveItemId, driveItemInvite)
238+
Expect(err).ToNot(HaveOccurred())
239+
Expect(utils.TSToTime(capturedReq.GetGrant().GetExpiration())).To(BeTemporally("~", explicit, time.Second))
240+
})
241+
177242
It("succeeds with file roles (happy path)", func() {
178243
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
179244
gatewayClient.On("CreateShare", mock.Anything, mock.Anything).Return(createShareResponse, nil)
@@ -349,6 +414,37 @@ var _ = Describe("DriveItemPermissionsService", func() {
349414
Expect(permission.GrantedToV2.User.GetDisplayName()).To(Equal(getUserResponse.User.DisplayName))
350415
Expect(permission.GrantedToV2.User.GetId()).To(Equal("1"))
351416
})
417+
It("does not apply a default expiration to a space membership created without one", func() {
418+
root := &provider.ResourceId{
419+
StorageId: "1",
420+
SpaceId: "2",
421+
OpaqueId: "2", // space root: OpaqueId == SpaceId
422+
}
423+
listSpacesResponse.StorageSpaces[0].SpaceType = "project"
424+
listSpacesResponse.StorageSpaces[0].Root = root
425+
statResponse.Info.Id = root
426+
statResponse.Info.Space = &provider.StorageSpace{Root: root}
427+
428+
var capturedReq *collaboration.CreateShareRequest
429+
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listSpacesResponse, nil)
430+
gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
431+
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(statResponse, nil)
432+
gatewayClient.On("CreateShare", mock.Anything, mock.Anything).
433+
Run(func(args mock.Arguments) {
434+
capturedReq = args.Get(1).(*collaboration.CreateShareRequest)
435+
}).Return(createShareResponse, nil)
436+
driveItemInvite.Recipients = []libregraph.DriveRecipient{
437+
{ObjectId: libregraph.PtrString("1"), LibreGraphRecipientType: libregraph.PtrString("user")},
438+
}
439+
driveItemInvite.ExpirationDateTime = nil
440+
createShareResponse.Share = &collaboration.Share{
441+
Id: &collaboration.ShareId{OpaqueId: "123"},
442+
}
443+
444+
_, err := driveItemPermissionsService.SpaceRootInvite(context.Background(), driveId, driveItemInvite)
445+
Expect(err).ToNot(HaveOccurred())
446+
Expect(capturedReq.GetGrant().GetExpiration()).To(BeNil())
447+
})
352448
It("rejects to add a user to a personal space", func() {
353449
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listSpacesResponse, nil)
354450
driveItemInvite.Recipients = []libregraph.DriveRecipient{
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
9+
"github.com/owncloud/ocis/v2/services/proxy/pkg/router"
10+
)
11+
12+
type stubAuth struct {
13+
name string
14+
err error // non-nil => this authenticator fails with this error
15+
calls *[]string
16+
}
17+
18+
func (s stubAuth) Authenticate(r *http.Request) (*http.Request, error) {
19+
*s.calls = append(*s.calls, s.name)
20+
if s.err != nil {
21+
return nil, s.err
22+
}
23+
return r, nil
24+
}
25+
26+
// A transient OIDC failure followed by a succeeding authenticator must serve 200,
27+
// not 503 — the deferral flag exists so authenticator order does not matter.
28+
func TestTransientThenSuccessServes200(t *testing.T) {
29+
var calls []string
30+
auths := []Authenticator{
31+
stubAuth{name: "oidc-transient", err: oidc.ErrTemporarilyUnavailable, calls: &calls},
32+
stubAuth{name: "public-share-ok", err: nil, calls: &calls},
33+
}
34+
35+
served := false
36+
handler := Authentication(auths, EnableBasicAuth(false))(
37+
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { served = true }),
38+
)
39+
40+
req := httptest.NewRequest(http.MethodGet, "http://example.com/dav/public-files/x", http.NoBody)
41+
req = req.WithContext(router.SetRoutingInfo(req.Context(), router.RoutingInfo{}))
42+
rr := httptest.NewRecorder()
43+
handler.ServeHTTP(rr, req)
44+
45+
t.Logf("authenticators called: %v, status: %d", calls, rr.Code)
46+
if !served || rr.Code != http.StatusOK {
47+
t.Fatalf("want 200 served by later authenticator, got status=%d served=%v (calls=%v)", rr.Code, served, calls)
48+
}
49+
}

‎tests/acceptance/features/coreApiShareUpdateToShares/updateShare.feature‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,6 @@ Feature: sharing
184184
| permissions | all |
185185
| stime | A_NUMBER |
186186
| parent | |
187-
| expiration | |
188187
| token | |
189188
| uid_file_owner | %username% |
190189
| displayname_file_owner | %displayname% |

0 commit comments

Comments
 (0)