Skip to content

Commit

Permalink
Remove space members (#2524)
Browse files Browse the repository at this point in the history
* remove owner from project spaces

* block removal of last space manager

The last manager of a space can not be removed.
If the user should be removed then a new manager need to be set.
After that the user can be removed.

* check node properties instead of create-space permission

* update integration tests

The integration tests needed to be updated since I removed the owner from project spaces.
Now the tests don't depend on an owner being set.

* add changelog

* add constants for space types and root node id
  • Loading branch information
David Christofas authored Feb 15, 2022
1 parent d217886 commit df1264d
Show file tree
Hide file tree
Showing 14 changed files with 129 additions and 74 deletions.
8 changes: 8 additions & 0 deletions changelog/unreleased/remove-space-members.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Enhancement: add checks when removing space members

- Removed owners from project spaces
- Prevent deletion of last space manager
- Viewers and editors can always be deleted
- Managers can only be deleted when there will be at least one remaining

https://github.com/cs3org/reva/pull/2524
38 changes: 3 additions & 35 deletions internal/http/services/owncloud/ocs/conversions/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ const (
RoleEditor = "editor"
// RoleFileEditor grants editor permission on a single file.
RoleFileEditor = "file-editor"
// RoleCoowner grants co-owner permissions on a resource.
RoleCoowner = "coowner"
// RoleUploader grants uploader permission to upload onto a resource.
RoleUploader = "uploader"
// RoleManager grants manager permissions on a resource. Semantically equivalent to co-owner.
Expand Down Expand Up @@ -127,8 +125,6 @@ func RoleFromName(name string) *Role {
return NewEditorRole()
case RoleFileEditor:
return NewFileEditorRole()
case RoleCoowner:
return NewCoownerRole()
case RoleUploader:
return NewUploaderRole()
case RoleManager:
Expand Down Expand Up @@ -211,34 +207,6 @@ func NewFileEditorRole() *Role {
}
}

// NewCoownerRole creates a coowner role
func NewCoownerRole() *Role {
return &Role{
Name: RoleCoowner,
cS3ResourcePermissions: &provider.ResourcePermissions{
GetPath: true,
GetQuota: true,
InitiateFileDownload: true,
ListGrants: true,
ListContainer: true,
ListFileVersions: true,
ListRecycle: true,
Stat: true,
InitiateFileUpload: true,
RestoreFileVersion: true,
RestoreRecycleItem: true,
CreateContainer: true,
Delete: true,
Move: true,
PurgeRecycle: true,
AddGrant: true,
UpdateGrant: true,
RemoveGrant: true,
},
ocsPermissions: PermissionAll,
}
}

// NewUploaderRole creates an uploader role
func NewUploaderRole() *Role {
return &Role{
Expand All @@ -254,7 +222,7 @@ func NewUploaderRole() *Role {
}
}

// NewManagerRole creates an editor role
// NewManagerRole creates an manager role
func NewManagerRole() *Role {
return &Role{
Name: RoleManager,
Expand Down Expand Up @@ -394,9 +362,9 @@ func RoleFromResourcePermissions(rp *provider.ResourcePermissions) *Role {
if r.ocsPermissions.Contain(PermissionWrite) && r.ocsPermissions.Contain(PermissionCreate) && r.ocsPermissions.Contain(PermissionDelete) {
r.Name = RoleEditor
if r.ocsPermissions.Contain(PermissionShare) {
r.Name = RoleCoowner
r.Name = RoleManager
}
return r // editor or coowner
return r // editor or manager
}
if r.ocsPermissions == PermissionRead {
r.Name = RoleViewer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,8 @@ func (h *Handler) CreateShare(w http.ResponseWriter, r *http.Request) {

switch shareType {
case int(conversions.ShareTypeUser), int(conversions.ShareTypeGroup):
// user collaborations default to coowner
role, val, ocsErr := h.extractPermissions(w, r, statRes.Info, conversions.NewCoownerRole())
// user collaborations default to Manager (=all permissions)
role, val, ocsErr := h.extractPermissions(w, r, statRes.Info, conversions.NewManagerRole())
if ocsErr != nil {
response.WriteOCSError(w, r, ocsErr.Code, ocsErr.Message, ocsErr.Error)
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,17 @@ func (h *Handler) removeSpaceMember(w http.ResponseWriter, r *http.Request, spac
return
}

lgRes, err := providerClient.ListGrants(ctx, &provider.ListGrantsRequest{Ref: &ref})
if err != nil || lgRes.Status.Code != rpc.Code_CODE_OK {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error listing space grants", err)
return
}

if len(lgRes.Grants) == 1 || !isSpaceManagerRemaining(lgRes.Grants, grantee) {
response.WriteOCSError(w, r, http.StatusForbidden, "can't remove the last manager", nil)
return
}

removeGrantRes, err := providerClient.RemoveGrant(ctx, &provider.RemoveGrantRequest{
Ref: &ref,
Grant: &provider.Grant{
Expand Down Expand Up @@ -233,3 +244,35 @@ func (h *Handler) findProvider(ctx context.Context, ref *provider.Reference) (*r

return res.Providers[0], nil
}

func isSpaceManagerRemaining(grants []*provider.Grant, grantee provider.Grantee) bool {
for _, g := range grants {
// AddGrant is currently the way to check for the manager role
// If it is not set than the current grant is not for a manager and
// we can just continue with the next one.
if g.Permissions.AddGrant && !isEqualGrantee(*g.Grantee, grantee) {
return true
}
}
return false
}

func isEqualGrantee(a, b provider.Grantee) bool {
// Ideally we would want to use utils.GranteeEqual()
// but the grants stored in the decomposedfs aren't complete (missing usertype and idp)
// because of that the check would fail so we can only check the ... for now.
if a.Type != b.Type {
return false
}

var aID, bID string
switch a.Type {
case provider.GranteeType_GRANTEE_TYPE_GROUP:
aID = a.GetGroupId().OpaqueId
bID = b.GetGroupId().OpaqueId
case provider.GranteeType_GRANTEE_TYPE_USER:
aID = a.GetUserId().OpaqueId
bID = b.GetUserId().OpaqueId
}
return aID == bID
}
3 changes: 2 additions & 1 deletion pkg/storage/registry/spaces/spaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ func (r *registry) GetProvider(ctx context.Context, space *providerpb.StorageSpa
continue
}
if space.Owner != nil {
spacePath, err = sc.SpacePath(nil, space)
user := ctxpkg.ContextMustGetUser(ctx)
spacePath, err = sc.SpacePath(user, space)
if err != nil {
continue
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/storage/utils/decomposedfs/decomposedfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ func (fs *Decomposedfs) CreateHome(ctx context.Context) (err error) {
}

// add storage space
if err := fs.createStorageSpace(ctx, "personal", h.ID); err != nil {
if err := fs.createStorageSpace(ctx, spaceTypePersonal, h.ID); err != nil {
return err
}

Expand Down
36 changes: 26 additions & 10 deletions pkg/storage/utils/decomposedfs/grants.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,31 @@ func (fs *Decomposedfs) AddGrant(ctx context.Context, ref *provider.Reference, g
return
}

ok, err := fs.p.HasPermission(ctx, node, func(rp *provider.ResourcePermissions) bool {
// TODO remove AddGrant or UpdateGrant grant from CS3 api, redundant? tracked in https://github.com/cs3org/cs3apis/issues/92
return rp.AddGrant || rp.UpdateGrant
})
switch {
case err != nil:
return errtypes.InternalError(err.Error())
case !ok:
return errtypes.PermissionDenied(filepath.Join(node.ParentID, node.Name))
grantees, err := node.ListGrantees(ctx)
if err != nil {
return err
}

owner, err := node.Owner()
if err != nil {
return err
}

// If the owner is empty and there are no grantees then we are dealing with a just created project space.
// In this case we don't need to check for permissions and just add the grant since this will be the project
// manager.
// When the owner is empty but grants are set then we do want to check the grants.
if !(len(grantees) == 0 && owner.OpaqueId == "") {
ok, err := fs.p.HasPermission(ctx, node, func(rp *provider.ResourcePermissions) bool {
// TODO remove AddGrant or UpdateGrant grant from CS3 api, redundant? tracked in https://github.com/cs3org/cs3apis/issues/92
return rp.AddGrant || rp.UpdateGrant
})
switch {
case err != nil:
return errtypes.InternalError(err.Error())
case !ok:
return errtypes.PermissionDenied(filepath.Join(node.ParentID, node.Name))
}
}

// check lock
Expand All @@ -75,7 +91,7 @@ func (fs *Decomposedfs) AddGrant(ctx context.Context, ref *provider.Reference, g

// when a grant is added to a space, do not add a new space under "shares"
if spaceGrant := ctx.Value(utils.SpaceGrant); spaceGrant == nil {
err := fs.createStorageSpace(ctx, "share", node.ID)
err := fs.createStorageSpace(ctx, spaceTypeShare, node.ID)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/storage/utils/decomposedfs/lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (lu *Lookup) Path(ctx context.Context, n *node.Node) (p string, err error)

// RootNode returns the root node of the storage
func (lu *Lookup) RootNode(ctx context.Context) (*node.Node, error) {
n := node.New("root", "", "", 0, "", nil, lu)
n := node.New(node.RootID, "", "", 0, "", nil, lu)
n.Exists = true
return n, nil
}
Expand Down
15 changes: 10 additions & 5 deletions pkg/storage/utils/decomposedfs/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ const (

// TrashIDDelimiter represents the characters used to separate the nodeid and the deletion time.
TrashIDDelimiter = ".T."

// RootID defines the root node's ID
RootID = "root"
)

// Node represents a node in the tree and provides methods to get a Parent or Child instance
Expand Down Expand Up @@ -803,16 +806,18 @@ func (n *Node) ReadUserPermissions(ctx context.Context, u *userpb.User) (ap prov
o, err := n.Owner()
if err != nil {
// TODO check if a parent folder has the owner set?
appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Msg("could not determine owner, returning default permissions")
appctx.GetLogger(ctx).Error().Err(err).Str("node", n.ID).Msg("could not determine owner, returning default permissions")
return NoPermissions(), err
}
if o.OpaqueId == "" {
// this happens for root nodes in the storage. the extended attributes are set to emptystring to indicate: no owner
// TODO what if no owner is set but grants are present?
return NoOwnerPermissions(), nil
// this happens for root nodes and project spaces in the storage. the extended attributes are set to emptystring to indicate: no owner
// for project spaces we need to go over the grants and check the granted permissions
if n.ID == RootID {
return NoOwnerPermissions(), nil
}
}
if utils.UserEqual(u.Id, o) {
appctx.GetLogger(ctx).Debug().Interface("node", n).Msg("user is owner, returning owner permissions")
appctx.GetLogger(ctx).Debug().Str("node", n.ID).Msg("user is owner, returning owner permissions")
return OwnerPermissions(), nil
}

Expand Down
19 changes: 12 additions & 7 deletions pkg/storage/utils/decomposedfs/node/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,18 @@ func (p *Permissions) AssemblePermissions(ctx context.Context, n *Node) (ap prov
return NoPermissions(), err
}
if o.OpaqueId == "" {
// this happens for root nodes in the storage. the extended attributes are set to emptystring to indicate: no owner
// TODO what if no owner is set but grants are present?
return NoOwnerPermissions(), nil
// this happens for root nodes and project spaces in the storage. the extended attributes are set to emptystring to indicate: no owner
// for project spaces we need to go over the grants and check the granted permissions
if n.ID == RootID {
return NoOwnerPermissions(), nil
}
}
if utils.UserEqual(u.Id, o) {
lp, err := n.lu.Path(ctx, n)
if err == nil && lp == n.lu.ShareFolder() {
return ShareFolderPermissions(), nil
}
appctx.GetLogger(ctx).Debug().Interface("node", n.ID).Msg("user is owner, returning owner permissions")
appctx.GetLogger(ctx).Debug().Str("node", n.ID).Msg("user is owner, returning owner permissions")
return OwnerPermissions(), nil
}
// determine root
Expand Down Expand Up @@ -276,13 +278,16 @@ func (p *Permissions) getUserAndPermissions(ctx context.Context, n *Node) (*user
return nil, &perms
}
if o.OpaqueId == "" {
// this happens for root nodes in the storage. the extended attributes are set to emptystring to indicate: no owner
// TODO what if no owner is set but grants are present?
// this happens for root nodes and project spaces in the storage. the extended attributes are set to emptystring to indicate: no owner
// for project spaces we need to go over the grants and check the granted permissions
if n.ID != RootID {
return u, nil
}
perms := NoOwnerPermissions()
return nil, &perms
}
if utils.UserEqual(u.Id, o) {
appctx.GetLogger(ctx).Debug().Interface("node", n.ID).Msg("user is owner, returning owner permissions")
appctx.GetLogger(ctx).Debug().Str("node", n.ID).Msg("user is owner, returning owner permissions")
perms := OwnerPermissions()
return u, &perms
}
Expand Down
26 changes: 18 additions & 8 deletions pkg/storage/utils/decomposedfs/spaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@ import (
)

const (
spaceTypeAny = "*"
spaceIDAny = "*"
spaceTypePersonal = "personal"
spaceTypeProject = "project"
spaceTypeShare = "share"
spaceTypeAny = "*"
spaceIDAny = "*"
)

// CreateStorageSpace creates a storage space
Expand Down Expand Up @@ -76,7 +79,7 @@ func (fs *Decomposedfs) CreateStorageSpace(ctx context.Context, req *provider.Cr
}
// TODO enforce a uuid?
// TODO clarify if we want to enforce a single personal storage space or if we want to allow sending the spaceid
if req.Type == "personal" {
if req.Type == spaceTypePersonal {
spaceID = req.Owner.Id.OpaqueId
}

Expand Down Expand Up @@ -109,7 +112,12 @@ func (fs *Decomposedfs) CreateStorageSpace(ctx context.Context, req *provider.Cr
return nil, fmt.Errorf("decomposedfs: spaces: contextual user not found")
}

if err := n.ChangeOwner(u.Id); err != nil {
ownerID := u.Id
if req.Type == spaceTypeProject {
ownerID = &userv1beta1.UserId{}
}

if err := n.ChangeOwner(ownerID); err != nil {
return nil, err
}

Expand Down Expand Up @@ -278,7 +286,7 @@ func (fs *Decomposedfs) ListStorageSpaces(ctx context.Context, filter []*provide
spaceType := filepath.Base(filepath.Dir(matches[i]))

// FIXME type share evolved to grant on the edge branch ... make it configurable if the driver should support them or not for now ... ignore type share
if spaceType == "share" {
if spaceType == spaceTypeShare {
numShares++
// do not list shares as spaces for the owner
continue
Expand Down Expand Up @@ -507,7 +515,7 @@ func (fs *Decomposedfs) DeleteStorageSpace(ctx context.Context, req *provider.De
return err
}

matches, err = filepath.Glob(filepath.Join(fs.o.Root, "nodes", "root", req.Id.OpaqueId+node.TrashIDDelimiter+"*"))
matches, err = filepath.Glob(filepath.Join(fs.o.Root, "nodes", node.RootID, req.Id.OpaqueId+node.TrashIDDelimiter+"*"))
if err != nil {
return err
}
Expand Down Expand Up @@ -659,8 +667,10 @@ func (fs *Decomposedfs) storageSpaceFromNode(ctx context.Context, n *node.Node,
}
}

space.Owner = &userv1beta1.User{ // FIXME only return a UserID, not a full blown user object
Id: owner,
if spaceType != spaceTypeProject && owner.OpaqueId != "" {
space.Owner = &userv1beta1.User{ // FIXME only return a UserID, not a full blown user object
Id: owner,
}
}

// we set the space mtime to the root item mtime
Expand Down
4 changes: 2 additions & 2 deletions pkg/storage/utils/decomposedfs/tree/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func (t *Tree) Setup(owner *userpb.UserId, propagateToRoot bool) error {

// the root node has an empty name
// the root node has no parent
n := node.New("root", "", "", 0, "", nil, t.lookup)
n := node.New(node.RootID, "", "", 0, "", nil, t.lookup)
err := t.createNode(n, owner)
if err != nil {
return err
Expand Down Expand Up @@ -207,7 +207,7 @@ func (t *Tree) linkSpace(spaceType, spaceID, nodeID string) {

func isRootNode(nodePath string) bool {
attr, err := xattrs.Get(nodePath, xattrs.ParentidAttr)
return err == nil && attr == "root"
return err == nil && attr == node.RootID
}
func isSharedNode(nodePath string) bool {
if attrs, err := xattr.List(nodePath); err == nil {
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/grpc/fixtures/gateway.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ home_template = "/users/{{.Id.OpaqueId}}"
"personal" = { "mount_point" = "/users", "path_template" = "/users/{{.Space.Owner.Id.OpaqueId}}" }

[grpc.services.storageregistry.drivers.spaces.providers."{{storage2_address}}".spaces]
"project" = { "mount_point" = "/users/[^/]+/Projects", "path_template" = "/users/{{.Space.Owner.Id.OpaqueId}}/Projects/{{.Space.Name}}" }
"project" = { "mount_point" = "/users/[^/]+/Projects", "path_template" = "/users/{{.CurrentUser.Id.OpaqueId}}/Projects/{{.Space.Name}}" }

[http]
address = "{{grpc_address+1}}"
Expand Down
Loading

0 comments on commit df1264d

Please sign in to comment.