Skip to content

Commit 2c7f578

Browse files
authored
feat: [OCISDEV-282] user type upgrade/downgrade (#11678)
* feat: [OCISDEV-282] user type upgrade/downgrade * the storage utils pkg separated * extended the test coverage * the failures added to expected
1 parent e4dec40 commit 2c7f578

File tree

10 files changed

+752
-30
lines changed

10 files changed

+752
-30
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Enhancement: Added user type upgrade/downgrade
2+
3+
Added the mechanism to disable/enable personal spaces on user type upgrade/downgrade
4+
5+
https://github.com/owncloud/ocis/pull/11678

ocis-pkg/shared/storage_utils.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package shared
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
9+
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
10+
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
11+
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
12+
libregraph "github.com/owncloud/libre-graph-api-go"
13+
"github.com/owncloud/ocis/v2/services/graph/pkg/errorcode"
14+
"github.com/owncloud/reva/v2/pkg/appctx"
15+
"github.com/owncloud/reva/v2/pkg/utils"
16+
)
17+
18+
const (
19+
_spaceTypePersonal = "personal"
20+
_spaceStateTrashed = "trashed"
21+
)
22+
23+
var (
24+
// ErrNotFound is returned when a personal space not found.
25+
ErrNotFound = errors.New("personal space not found")
26+
)
27+
28+
// DisablePersonalSpace disables (deletes) the personal storage space for the given userID.
29+
// If the personal space is already deleted (trashed), it is a no-op.
30+
// Returns an error if the space cannot be found or the deletion fails.
31+
func DisablePersonalSpace(ctx context.Context, client gateway.GatewayAPIClient, userID string) error {
32+
logger := appctx.GetLogger(ctx)
33+
sp, err := getPersonalSpace(ctx, client, userID)
34+
if err != nil {
35+
return fmt.Errorf("failed to retrieve personal space: %w", err)
36+
}
37+
if isTrashed(sp) {
38+
logger.Debug().Str("userID", userID).Msg("the personal space already deleted")
39+
return nil
40+
}
41+
42+
dRes, derr := client.DeleteStorageSpace(ctx, &storageprovider.DeleteStorageSpaceRequest{
43+
Id: &storageprovider.StorageSpaceId{OpaqueId: sp.GetId().GetOpaqueId()},
44+
})
45+
if derr != nil {
46+
return fmt.Errorf("failed to disable personal space: %w", derr)
47+
}
48+
if dRes.GetStatus().GetCode() != cs3rpc.Code_CODE_OK {
49+
return errorcode.NewFromStatusCode(dRes.GetStatus().GetCode(), dRes.GetStatus().GetMessage())
50+
}
51+
return nil
52+
}
53+
54+
// EnsurePersonalSpace ensures that a personal storage space exists and is enabled for the given user.
55+
// If the personal space is found and is trashed, it restores it.
56+
// If the personal space does not exist, it creates a new one.
57+
// Returns an error if the operation fails.
58+
func EnsurePersonalSpace(ctx context.Context, client gateway.GatewayAPIClient, user libregraph.User) error {
59+
sp, err := getPersonalSpace(ctx, client, user.GetId())
60+
if err != nil && !errors.Is(err, ErrNotFound) {
61+
return err
62+
}
63+
64+
if sp != nil && isTrashed(sp) {
65+
req := &storageprovider.UpdateStorageSpaceRequest{
66+
Opaque: utils.AppendPlainToOpaque(nil, "restore", "true"),
67+
StorageSpace: &storageprovider.StorageSpace{
68+
Id: &storageprovider.StorageSpaceId{OpaqueId: sp.GetId().GetOpaqueId()},
69+
Root: sp.GetRoot(),
70+
},
71+
}
72+
uRes, uerr := client.UpdateStorageSpace(ctx, req)
73+
if uerr != nil {
74+
return fmt.Errorf("failed to enable personal space: %w", uerr)
75+
}
76+
if uRes.GetStatus().GetCode() != cs3rpc.Code_CODE_OK {
77+
return errorcode.NewFromStatusCode(uRes.GetStatus().GetCode(), uRes.GetStatus().GetMessage())
78+
}
79+
return nil
80+
}
81+
if errors.Is(err, ErrNotFound) {
82+
req := &storageprovider.CreateStorageSpaceRequest{
83+
Type: _spaceTypePersonal,
84+
Owner: &userv1beta1.User{Id: &userv1beta1.UserId{OpaqueId: user.GetId()}},
85+
Name: user.GetDisplayName(),
86+
}
87+
cRes, cerr := client.CreateStorageSpace(ctx, req)
88+
if cerr != nil {
89+
return fmt.Errorf("failed to create personal space: %w", cerr)
90+
}
91+
if cRes.GetStatus().GetCode() == cs3rpc.Code_CODE_ALREADY_EXISTS {
92+
return nil
93+
}
94+
if cRes.GetStatus().GetCode() != cs3rpc.Code_CODE_OK {
95+
return errorcode.NewFromStatusCode(cRes.GetStatus().GetCode(), cRes.GetStatus().GetMessage())
96+
}
97+
}
98+
return nil
99+
}
100+
101+
func getPersonalSpace(ctx context.Context, client gateway.GatewayAPIClient, userID string) (*storageprovider.StorageSpace, error) {
102+
lspr, err := client.ListStorageSpaces(ctx, &storageprovider.ListStorageSpacesRequest{
103+
Opaque: utils.AppendPlainToOpaque(nil, "unrestricted", "T"),
104+
Filters: []*storageprovider.ListStorageSpacesRequest_Filter{
105+
listStorageSpacesUserFilter(userID),
106+
listStorageSpacesTypeFilter(_spaceTypePersonal)},
107+
})
108+
if err != nil {
109+
return nil, fmt.Errorf("failed to retrieve personal space: %w", err)
110+
}
111+
if lspr.GetStatus().GetCode() != cs3rpc.Code_CODE_OK {
112+
return nil, fmt.Errorf("failed to retrieve personal space: %s", lspr.GetStatus().GetMessage())
113+
}
114+
if len(lspr.GetStorageSpaces()) > 1 {
115+
return nil, errors.New("retrieved more than one personal space")
116+
}
117+
if len(lspr.GetStorageSpaces()) != 1 {
118+
return nil, ErrNotFound
119+
}
120+
return lspr.GetStorageSpaces()[0], nil
121+
}
122+
123+
func listStorageSpacesUserFilter(id string) *storageprovider.ListStorageSpacesRequest_Filter {
124+
return &storageprovider.ListStorageSpacesRequest_Filter{
125+
Type: storageprovider.ListStorageSpacesRequest_Filter_TYPE_USER,
126+
Term: &storageprovider.ListStorageSpacesRequest_Filter_User{
127+
User: &userv1beta1.UserId{
128+
OpaqueId: id,
129+
},
130+
},
131+
}
132+
}
133+
134+
func listStorageSpacesTypeFilter(spaceType string) *storageprovider.ListStorageSpacesRequest_Filter {
135+
return &storageprovider.ListStorageSpacesRequest_Filter{
136+
Type: storageprovider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE,
137+
Term: &storageprovider.ListStorageSpacesRequest_Filter_SpaceType{
138+
SpaceType: spaceType,
139+
},
140+
}
141+
}
142+
143+
func isTrashed(sp *storageprovider.StorageSpace) bool {
144+
return utils.ReadPlainFromOpaque(sp.GetOpaque(), _spaceStateTrashed) == _spaceStateTrashed
145+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package shared
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
8+
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
9+
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
10+
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
11+
libregraph "github.com/owncloud/libre-graph-api-go"
12+
cs3mocks "github.com/owncloud/reva/v2/tests/cs3mocks/mocks"
13+
"github.com/stretchr/testify/mock"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func okStatus() *cs3rpc.Status {
18+
return &cs3rpc.Status{Code: cs3rpc.Code_CODE_OK}
19+
}
20+
21+
func status(code cs3rpc.Code, msg string) *cs3rpc.Status {
22+
return &cs3rpc.Status{Code: code, Message: msg}
23+
}
24+
25+
func newSpace(id string, trashed bool) *storageprovider.StorageSpace {
26+
sp := &storageprovider.StorageSpace{
27+
Id: &storageprovider.StorageSpaceId{OpaqueId: id},
28+
Root: nil,
29+
}
30+
if trashed {
31+
sp.Opaque = &types.Opaque{
32+
Map: map[string]*types.OpaqueEntry{
33+
_spaceStateTrashed: {Decoder: "plain", Value: []byte(_spaceStateTrashed)},
34+
},
35+
}
36+
}
37+
return sp
38+
}
39+
40+
func TestEnsurePersonalSpace(t *testing.T) {
41+
t.Run("no-op when personal space exists and is active", func(t *testing.T) {
42+
gw := cs3mocks.NewGatewayAPIClient(t)
43+
gw.EXPECT().ListStorageSpaces(mock.Anything, mock.Anything, mock.Anything).Return(&storageprovider.ListStorageSpacesResponse{
44+
Status: okStatus(),
45+
StorageSpaces: []*storageprovider.StorageSpace{newSpace("ps1", false)},
46+
}, nil).Once()
47+
48+
user := libregraph.NewUser("User One", "user1")
49+
user.SetId("user1")
50+
51+
err := EnsurePersonalSpace(context.Background(), gw, *user)
52+
require.NoError(t, err)
53+
})
54+
55+
t.Run("restores trashed personal space", func(t *testing.T) {
56+
gw := cs3mocks.NewGatewayAPIClient(t)
57+
sp := newSpace("ps2", true)
58+
59+
gw.EXPECT().ListStorageSpaces(mock.Anything, mock.Anything, mock.Anything).Return(&storageprovider.ListStorageSpacesResponse{
60+
Status: okStatus(),
61+
StorageSpaces: []*storageprovider.StorageSpace{sp},
62+
}, nil).Once()
63+
64+
gw.EXPECT().UpdateStorageSpace(mock.Anything, mock.Anything, mock.Anything).Return(&storageprovider.UpdateStorageSpaceResponse{
65+
Status: okStatus(),
66+
}, nil).Once()
67+
68+
user := libregraph.NewUser("User Two", "user2")
69+
user.SetId("user2")
70+
71+
err := EnsurePersonalSpace(context.Background(), gw, *user)
72+
require.NoError(t, err)
73+
})
74+
75+
t.Run("creates when not found", func(t *testing.T) {
76+
gw := cs3mocks.NewGatewayAPIClient(t)
77+
gw.EXPECT().ListStorageSpaces(mock.Anything, mock.Anything, mock.Anything).Return(&storageprovider.ListStorageSpacesResponse{
78+
Status: okStatus(),
79+
StorageSpaces: []*storageprovider.StorageSpace{},
80+
}, nil).Once()
81+
82+
gw.EXPECT().CreateStorageSpace(mock.Anything, mock.Anything, mock.Anything).Return(&storageprovider.CreateStorageSpaceResponse{
83+
Status: okStatus(),
84+
}, nil).Once()
85+
86+
user := libregraph.NewUser("User Three", "user3")
87+
user.SetId("user3")
88+
89+
err := EnsurePersonalSpace(context.Background(), gw, *user)
90+
require.NoError(t, err)
91+
})
92+
93+
t.Run("create returns already exists treated as success", func(t *testing.T) {
94+
gw := cs3mocks.NewGatewayAPIClient(t)
95+
gw.EXPECT().ListStorageSpaces(mock.Anything, mock.Anything, mock.Anything).Return(&storageprovider.ListStorageSpacesResponse{
96+
Status: okStatus(),
97+
StorageSpaces: []*storageprovider.StorageSpace{},
98+
}, nil).Once()
99+
100+
gw.EXPECT().CreateStorageSpace(mock.Anything, mock.Anything, mock.Anything).Return(&storageprovider.CreateStorageSpaceResponse{
101+
Status: status(cs3rpc.Code_CODE_ALREADY_EXISTS, "exists"),
102+
}, nil).Once()
103+
104+
user := libregraph.NewUser("User Four", "user4")
105+
user.SetId("user4")
106+
107+
err := EnsurePersonalSpace(context.Background(), gw, *user)
108+
require.NoError(t, err)
109+
})
110+
111+
t.Run("propagates list error", func(t *testing.T) {
112+
gw := cs3mocks.NewGatewayAPIClient(t)
113+
gw.EXPECT().ListStorageSpaces(mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("boom")).Once()
114+
115+
user := libregraph.NewUser("User Five", "user5")
116+
user.SetId("user5")
117+
118+
err := EnsurePersonalSpace(context.Background(), gw, *user)
119+
require.Error(t, err)
120+
})
121+
122+
t.Run("propagates non-ok update status", func(t *testing.T) {
123+
gw := cs3mocks.NewGatewayAPIClient(t)
124+
sp := newSpace("ps3", true)
125+
126+
gw.EXPECT().ListStorageSpaces(mock.Anything, mock.Anything, mock.Anything).Return(&storageprovider.ListStorageSpacesResponse{
127+
Status: okStatus(),
128+
StorageSpaces: []*storageprovider.StorageSpace{sp},
129+
}, nil).Once()
130+
131+
gw.EXPECT().UpdateStorageSpace(mock.Anything, mock.Anything, mock.Anything).Return(&storageprovider.UpdateStorageSpaceResponse{
132+
Status: status(cs3rpc.Code_CODE_INVALID_ARGUMENT, "nope"),
133+
}, nil).Once()
134+
135+
user := libregraph.NewUser("User Six", "user6")
136+
user.SetId("user6")
137+
138+
err := EnsurePersonalSpace(context.Background(), gw, *user)
139+
require.Error(t, err)
140+
})
141+
}
142+
143+
func TestDisablePersonalSpace(t *testing.T) {
144+
t.Run("no-op when personal space already trashed", func(t *testing.T) {
145+
gw := cs3mocks.NewGatewayAPIClient(t)
146+
sp := newSpace("ps4", true)
147+
gw.EXPECT().ListStorageSpaces(mock.Anything, mock.Anything, mock.Anything).Return(&storageprovider.ListStorageSpacesResponse{
148+
Status: okStatus(),
149+
StorageSpaces: []*storageprovider.StorageSpace{sp},
150+
}, nil).Once()
151+
152+
err := DisablePersonalSpace(context.Background(), gw, "user1")
153+
require.NoError(t, err)
154+
})
155+
156+
t.Run("deletes active personal space", func(t *testing.T) {
157+
gw := cs3mocks.NewGatewayAPIClient(t)
158+
sp := newSpace("ps5", false)
159+
gw.EXPECT().ListStorageSpaces(mock.Anything, mock.Anything, mock.Anything).Return(&storageprovider.ListStorageSpacesResponse{
160+
Status: okStatus(),
161+
StorageSpaces: []*storageprovider.StorageSpace{sp},
162+
}, nil).Once()
163+
164+
gw.EXPECT().DeleteStorageSpace(mock.Anything, mock.Anything, mock.Anything).Return(&storageprovider.DeleteStorageSpaceResponse{
165+
Status: okStatus(),
166+
}, nil).Once()
167+
168+
err := DisablePersonalSpace(context.Background(), gw, "user1")
169+
require.NoError(t, err)
170+
})
171+
172+
t.Run("propagates non-ok delete status", func(t *testing.T) {
173+
gw := cs3mocks.NewGatewayAPIClient(t)
174+
sp := newSpace("ps6", false)
175+
gw.EXPECT().ListStorageSpaces(mock.Anything, mock.Anything, mock.Anything).Return(&storageprovider.ListStorageSpacesResponse{
176+
Status: okStatus(),
177+
StorageSpaces: []*storageprovider.StorageSpace{sp},
178+
}, nil).Once()
179+
180+
gw.EXPECT().DeleteStorageSpace(mock.Anything, mock.Anything, mock.Anything).Return(&storageprovider.DeleteStorageSpaceResponse{
181+
Status: status(cs3rpc.Code_CODE_INTERNAL, "fail"),
182+
}, nil).Once()
183+
184+
err := DisablePersonalSpace(context.Background(), gw, "user1")
185+
require.Error(t, err)
186+
})
187+
188+
t.Run("propagates list error", func(t *testing.T) {
189+
gw := cs3mocks.NewGatewayAPIClient(t)
190+
gw.EXPECT().ListStorageSpaces(mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("boom")).Once()
191+
192+
err := DisablePersonalSpace(context.Background(), gw, "user1")
193+
require.Error(t, err)
194+
})
195+
}

services/graph/pkg/errorcode/errorcode.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http"
88
"time"
99

10+
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
1011
"github.com/go-chi/chi/v5/middleware"
1112
"github.com/go-chi/render"
1213
libregraph "github.com/owncloud/libre-graph-api-go"
@@ -106,6 +107,11 @@ func New(e ErrorCode, msg string) Error {
106107
}
107108
}
108109

110+
// New errorcode.Error from cs3rpc Status Code
111+
func NewFromStatusCode(code rpc.Code, msg string) Error {
112+
return New(cs3StatusToErrCode(code), msg)
113+
}
114+
109115
// Render writes a Graph ErrorCode object to the response writer
110116
func (e ErrorCode) Render(w http.ResponseWriter, r *http.Request, status int, msg string) {
111117
render.Status(r, status)
@@ -206,3 +212,23 @@ func ToError(err error) (Error, bool) {
206212

207213
return Error{}, false
208214
}
215+
216+
func cs3StatusToErrCode(code rpc.Code) (errcode ErrorCode) {
217+
switch code {
218+
case rpc.Code_CODE_UNAUTHENTICATED:
219+
errcode = Unauthenticated
220+
case rpc.Code_CODE_PERMISSION_DENIED:
221+
errcode = AccessDenied
222+
case rpc.Code_CODE_NOT_FOUND:
223+
errcode = ItemNotFound
224+
case rpc.Code_CODE_LOCKED:
225+
errcode = ItemIsLocked
226+
case rpc.Code_CODE_INVALID_ARGUMENT:
227+
errcode = InvalidRequest
228+
case rpc.Code_CODE_FAILED_PRECONDITION:
229+
errcode = InvalidRequest
230+
default:
231+
errcode = GeneralException
232+
}
233+
return errcode
234+
}

0 commit comments

Comments
 (0)