diff --git a/changelog/unreleased/check-permissions-in-ocis-driver.md b/changelog/unreleased/check-permissions-in-ocis-driver.md new file mode 100644 index 0000000000..b81c813839 --- /dev/null +++ b/changelog/unreleased/check-permissions-in-ocis-driver.md @@ -0,0 +1,5 @@ +Enhancement: check permissions in ocis driver + +We are now checking grant permissions in the ocis storage driver. + +https://github.com/cs3org/reva/pull/1213 diff --git a/pkg/storage/fs/ocis/grants.go b/pkg/storage/fs/ocis/grants.go index e08e52045e..cfef106019 100644 --- a/pkg/storage/fs/ocis/grants.go +++ b/pkg/storage/fs/ocis/grants.go @@ -34,7 +34,7 @@ func (fs *ocisfs) AddGrant(ctx context.Context, ref *provider.Reference, g *prov log := appctx.GetLogger(ctx) log.Debug().Interface("ref", ref).Interface("grant", g).Msg("AddGrant()") var node *Node - if node, err = fs.pw.NodeFromResource(ctx, ref); err != nil { + if node, err = fs.lu.NodeFromResource(ctx, ref); err != nil { return } if !node.Exists { @@ -42,10 +42,21 @@ func (fs *ocisfs) AddGrant(ctx context.Context, ref *provider.Reference, g *prov return } - np := filepath.Join(fs.pw.Root, "nodes", node.ID) + 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)) + } + + np := fs.lu.toInternalPath(node.ID) e := ace.FromGrant(g) principal, value := e.Marshal() - if err := xattr.Set(np, sharePrefix+principal, value); err != nil { + if err := xattr.Set(np, grantPrefix+principal, value); err != nil { return err } return fs.tp.Propagate(ctx, node) @@ -53,15 +64,26 @@ func (fs *ocisfs) AddGrant(ctx context.Context, ref *provider.Reference, g *prov func (fs *ocisfs) ListGrants(ctx context.Context, ref *provider.Reference) (grants []*provider.Grant, err error) { var node *Node - if node, err = fs.pw.NodeFromResource(ctx, ref); err != nil { + if node, err = fs.lu.NodeFromResource(ctx, ref); err != nil { return } if !node.Exists { err = errtypes.NotFound(filepath.Join(node.ParentID, node.Name)) return } + + ok, err := fs.p.HasPermission(ctx, node, func(rp *provider.ResourcePermissions) bool { + return rp.ListGrants + }) + switch { + case err != nil: + return nil, errtypes.InternalError(err.Error()) + case !ok: + return nil, errtypes.PermissionDenied(filepath.Join(node.ParentID, node.Name)) + } + log := appctx.GetLogger(ctx) - np := filepath.Join(fs.pw.Root, "nodes", node.ID) + np := fs.lu.toInternalPath(node.ID) var attrs []string if attrs, err = xattr.List(np); err != nil { log.Error().Err(err).Msg("error listing attributes") @@ -82,7 +104,7 @@ func (fs *ocisfs) ListGrants(ctx context.Context, ref *provider.Reference) (gran func (fs *ocisfs) RemoveGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) (err error) { var node *Node - if node, err = fs.pw.NodeFromResource(ctx, ref); err != nil { + if node, err = fs.lu.NodeFromResource(ctx, ref); err != nil { return } if !node.Exists { @@ -90,14 +112,24 @@ func (fs *ocisfs) RemoveGrant(ctx context.Context, ref *provider.Reference, g *p return } + ok, err := fs.p.HasPermission(ctx, node, func(rp *provider.ResourcePermissions) bool { + return rp.RemoveGrant + }) + switch { + case err != nil: + return errtypes.InternalError(err.Error()) + case !ok: + return errtypes.PermissionDenied(filepath.Join(node.ParentID, node.Name)) + } + var attr string if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP { - attr = sharePrefix + "g:" + g.Grantee.Id.OpaqueId + attr = grantPrefix + "g:" + g.Grantee.Id.OpaqueId } else { - attr = sharePrefix + "u:" + g.Grantee.Id.OpaqueId + attr = grantPrefix + "u:" + g.Grantee.Id.OpaqueId } - np := filepath.Join(fs.pw.Root, "nodes", node.ID) + np := fs.lu.toInternalPath(node.ID) if err = xattr.Remove(np, attr); err != nil { return } @@ -106,6 +138,7 @@ func (fs *ocisfs) RemoveGrant(ctx context.Context, ref *provider.Reference, g *p } func (fs *ocisfs) UpdateGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error { + // TODO remove AddGrant or UpdateGrant grant from CS3 api, redundant? tracked in https://github.com/cs3org/cs3apis/issues/92 return fs.AddGrant(ctx, ref, g) } @@ -114,7 +147,7 @@ func extractACEsFromAttrs(ctx context.Context, fsfn string, attrs []string) (ent log := appctx.GetLogger(ctx) entries = []*ace.ACE{} for i := range attrs { - if strings.HasPrefix(attrs[i], sharePrefix) { + if strings.HasPrefix(attrs[i], grantPrefix) { var value []byte var err error if value, err = xattr.Get(fsfn, attrs[i]); err != nil { @@ -122,7 +155,7 @@ func extractACEsFromAttrs(ctx context.Context, fsfn string, attrs []string) (ent continue } var e *ace.ACE - principal := attrs[i][len(sharePrefix):] + principal := attrs[i][len(grantPrefix):] if e, err = ace.Unmarshal(principal, value); err != nil { log.Error().Err(err).Str("principal", principal).Str("attr", attrs[i]).Msg("could unmarshal ace") continue diff --git a/pkg/storage/fs/ocis/persistence.go b/pkg/storage/fs/ocis/interfaces.go similarity index 89% rename from pkg/storage/fs/ocis/persistence.go rename to pkg/storage/fs/ocis/interfaces.go index 5b9dcc16fc..b140aafc9a 100644 --- a/pkg/storage/fs/ocis/persistence.go +++ b/pkg/storage/fs/ocis/interfaces.go @@ -25,6 +25,9 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" ) +// TODO the different aspects of a storage: Tree, Lookup and Permissions should be able to be reusable +// Below is a start of Interfaces that needs to be worked out further + // TreePersistence is used to manage a tree hierarchy type TreePersistence interface { GetPathByID(ctx context.Context, id *provider.ResourceId) (string, error) @@ -39,8 +42,9 @@ type TreePersistence interface { Propagate(ctx context.Context, node *Node) (err error) } -// PathWrapper is used to encapsulate path transformations -type PathWrapper interface { +// Lookup is used to encapsulate path transformations +/* +type Lookup interface { NodeFromResource(ctx context.Context, ref *provider.Reference) (node *Node, err error) NodeFromID(ctx context.Context, id *provider.ResourceId) (node *Node, err error) NodeFromPath(ctx context.Context, fn string) (node *Node, err error) @@ -57,3 +61,4 @@ type PathWrapper interface { // it returns the storages root node otherwise HomeOrRootNode(ctx context.Context) (node *Node, err error) } +*/ diff --git a/pkg/storage/fs/ocis/path.go b/pkg/storage/fs/ocis/lookup.go similarity index 58% rename from pkg/storage/fs/ocis/path.go rename to pkg/storage/fs/ocis/lookup.go index 4573e41151..2a189095b1 100644 --- a/pkg/storage/fs/ocis/path.go +++ b/pkg/storage/fs/ocis/lookup.go @@ -31,35 +31,19 @@ import ( "github.com/cs3org/reva/pkg/user" ) -// Path implements transformations from filepath to node and back -type Path struct { - // ocis fs works on top of a dir of uuid nodes - Root string `mapstructure:"root"` - - // UserLayout describes the relative path from the storage's root node to the users home node. - UserLayout string `mapstructure:"user_layout"` - - // TODO NodeLayout option to save nodes as eg. nodes/1d/d8/1dd84abf-9466-4e14-bb86-02fc4ea3abcf - ShareFolder string `mapstructure:"share_folder"` - - // EnableHome enables the creation of home directories. - EnableHome bool `mapstructure:"enable_home"` - - // propagate mtime changes as tmtime (tree modification time) to the parent directory when user.ocis.propagation=1 is set on a node - TreeTimeAccounting bool `mapstructure:"treetime_accounting"` - - // propagate size changes as treesize - TreeSizeAccounting bool `mapstructure:"treesize_accounting"` +// Lookup implements transformations from filepath to node and back +type Lookup struct { + Options *Options } // NodeFromResource takes in a request path or request id and converts it to a Node -func (pw *Path) NodeFromResource(ctx context.Context, ref *provider.Reference) (*Node, error) { +func (lu *Lookup) NodeFromResource(ctx context.Context, ref *provider.Reference) (*Node, error) { if ref.GetPath() != "" { - return pw.NodeFromPath(ctx, ref.GetPath()) + return lu.NodeFromPath(ctx, ref.GetPath()) } if ref.GetId() != nil { - return pw.NodeFromID(ctx, ref.GetId()) + return lu.NodeFromID(ctx, ref.GetId()) } // reference is invalid @@ -67,16 +51,16 @@ func (pw *Path) NodeFromResource(ctx context.Context, ref *provider.Reference) ( } // NodeFromPath converts a filename into a Node -func (pw *Path) NodeFromPath(ctx context.Context, fn string) (node *Node, err error) { +func (lu *Lookup) NodeFromPath(ctx context.Context, fn string) (node *Node, err error) { log := appctx.GetLogger(ctx) log.Debug().Interface("fn", fn).Msg("NodeFromPath()") - if node, err = pw.HomeOrRootNode(ctx); err != nil { + if node, err = lu.HomeOrRootNode(ctx); err != nil { return } if fn != "/" { - node, err = pw.WalkPath(ctx, node, fn, func(ctx context.Context, n *Node) error { + node, err = lu.WalkPath(ctx, node, fn, func(ctx context.Context, n *Node) error { log.Debug().Interface("node", n).Msg("NodeFromPath() walk") return nil }) @@ -86,17 +70,17 @@ func (pw *Path) NodeFromPath(ctx context.Context, fn string) (node *Node, err er } // NodeFromID returns the internal path for the id -func (pw *Path) NodeFromID(ctx context.Context, id *provider.ResourceId) (n *Node, err error) { +func (lu *Lookup) NodeFromID(ctx context.Context, id *provider.ResourceId) (n *Node, err error) { if id == nil || id.OpaqueId == "" { return nil, fmt.Errorf("invalid resource id %+v", id) } - return ReadNode(ctx, pw, id.OpaqueId) + return ReadNode(ctx, lu, id.OpaqueId) } // Path returns the path for node -func (pw *Path) Path(ctx context.Context, n *Node) (p string, err error) { +func (lu *Lookup) Path(ctx context.Context, n *Node) (p string, err error) { var root *Node - if root, err = pw.HomeOrRootNode(ctx); err != nil { + if root, err = lu.HomeOrRootNode(ctx); err != nil { return } for n.ID != root.ID { @@ -114,9 +98,9 @@ func (pw *Path) Path(ctx context.Context, n *Node) (p string, err error) { } // RootNode returns the root node of the storage -func (pw *Path) RootNode(ctx context.Context) (node *Node, err error) { +func (lu *Lookup) RootNode(ctx context.Context) (node *Node, err error) { return &Node{ - pw: pw, + lu: lu, ID: "root", Name: "", ParentID: "", @@ -125,21 +109,21 @@ func (pw *Path) RootNode(ctx context.Context) (node *Node, err error) { } // HomeNode returns the home node of a user -func (pw *Path) HomeNode(ctx context.Context) (node *Node, err error) { - if !pw.EnableHome { +func (lu *Lookup) HomeNode(ctx context.Context) (node *Node, err error) { + if !lu.Options.EnableHome { return nil, errtypes.NotSupported("ocisfs: home supported disabled") } - if node, err = pw.RootNode(ctx); err != nil { + if node, err = lu.RootNode(ctx); err != nil { return } - node, err = pw.WalkPath(ctx, node, pw.mustGetUserLayout(ctx), nil) + node, err = lu.WalkPath(ctx, node, lu.mustGetUserLayout(ctx), nil) return } // WalkPath calls n.Child(segment) on every path segment in p starting at the node r // If a function f is given it will be executed for every segment node, but not the root node r -func (pw *Path) WalkPath(ctx context.Context, r *Node, p string, f func(ctx context.Context, n *Node) error) (*Node, error) { +func (lu *Lookup) WalkPath(ctx context.Context, r *Node, p string, f func(ctx context.Context, n *Node) error) (*Node, error) { segments := strings.Split(strings.Trim(p, "/"), "/") var err error for i := range segments { @@ -161,14 +145,18 @@ func (pw *Path) WalkPath(ctx context.Context, r *Node, p string, f func(ctx cont // HomeOrRootNode returns the users home node when home support is enabled. // it returns the storages root node otherwise -func (pw *Path) HomeOrRootNode(ctx context.Context) (node *Node, err error) { - if pw.EnableHome { - return pw.HomeNode(ctx) +func (lu *Lookup) HomeOrRootNode(ctx context.Context) (node *Node, err error) { + if lu.Options.EnableHome { + return lu.HomeNode(ctx) } - return pw.RootNode(ctx) + return lu.RootNode(ctx) } -func (pw *Path) mustGetUserLayout(ctx context.Context) string { +func (lu *Lookup) mustGetUserLayout(ctx context.Context) string { u := user.ContextMustGetUser(ctx) - return templates.WithUser(u, pw.UserLayout) + return templates.WithUser(u, lu.Options.UserLayout) +} + +func (lu *Lookup) toInternalPath(id string) string { + return filepath.Join(lu.Options.Root, "nodes", id) } diff --git a/pkg/storage/fs/ocis/metadata.go b/pkg/storage/fs/ocis/metadata.go index 0d5859f3c0..68880be98e 100644 --- a/pkg/storage/fs/ocis/metadata.go +++ b/pkg/storage/fs/ocis/metadata.go @@ -30,7 +30,7 @@ import ( ) func (fs *ocisfs) SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) (err error) { - n, err := fs.pw.NodeFromResource(ctx, ref) + n, err := fs.lu.NodeFromResource(ctx, ref) if err != nil { return errors.Wrap(err, "ocisfs: error resolving ref") } @@ -39,7 +39,19 @@ func (fs *ocisfs) SetArbitraryMetadata(ctx context.Context, ref *provider.Refere err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name)) return err } - nodePath := filepath.Join(fs.pw.Root, "nodes", n.ID) + + ok, err := fs.p.HasPermission(ctx, n, func(rp *provider.ResourcePermissions) bool { + // TODO add explicit SetArbitraryMetadata grant to CS3 api, tracked in https://github.com/cs3org/cs3apis/issues/91 + return rp.InitiateFileUpload + }) + switch { + case err != nil: + return errtypes.InternalError(err.Error()) + case !ok: + return errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name)) + } + + nodePath := n.lu.toInternalPath(n.ID) for k, v := range md.Metadata { // TODO set etag as temporary etag tmpEtagAttr attrName := metadataPrefix + k @@ -51,7 +63,7 @@ func (fs *ocisfs) SetArbitraryMetadata(ctx context.Context, ref *provider.Refere } func (fs *ocisfs) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Reference, keys []string) (err error) { - n, err := fs.pw.NodeFromResource(ctx, ref) + n, err := fs.lu.NodeFromResource(ctx, ref) if err != nil { return errors.Wrap(err, "ocisfs: error resolving ref") } @@ -60,7 +72,19 @@ func (fs *ocisfs) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Refe err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name)) return err } - nodePath := filepath.Join(fs.pw.Root, "nodes", n.ID) + + ok, err := fs.p.HasPermission(ctx, n, func(rp *provider.ResourcePermissions) bool { + // TODO use SetArbitraryMetadata grant to CS3 api, tracked in https://github.com/cs3org/cs3apis/issues/91 + return rp.InitiateFileUpload + }) + switch { + case err != nil: + return errtypes.InternalError(err.Error()) + case !ok: + return errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name)) + } + + nodePath := n.lu.toInternalPath(n.ID) for i := range keys { attrName := metadataPrefix + keys[i] if err = xattr.Remove(nodePath, attrName); err != nil { diff --git a/pkg/storage/fs/ocis/node.go b/pkg/storage/fs/ocis/node.go index 97f463cb83..fd9e2e8099 100644 --- a/pkg/storage/fs/ocis/node.go +++ b/pkg/storage/fs/ocis/node.go @@ -32,14 +32,17 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/mime" + "github.com/cs3org/reva/pkg/storage/utils/ace" "github.com/pkg/errors" "github.com/pkg/xattr" + "github.com/rs/zerolog/log" ) // Node represents a node in the tree and provides methods to get a Parent or Child instance type Node struct { - pw *Path + lu *Lookup ParentID string ID string Name string @@ -49,14 +52,21 @@ type Node struct { } func (n *Node) writeMetadata(owner *userpb.UserId) (err error) { - nodePath := filepath.Join(n.pw.Root, "nodes", n.ID) + nodePath := n.lu.toInternalPath(n.ID) if err = xattr.Set(nodePath, parentidAttr, []byte(n.ParentID)); err != nil { return errors.Wrap(err, "ocisfs: could not set parentid attribute") } if err = xattr.Set(nodePath, nameAttr, []byte(n.Name)); err != nil { return errors.Wrap(err, "ocisfs: could not set name attribute") } - if owner != nil { + if owner == nil { + if err = xattr.Set(nodePath, ownerIDAttr, []byte("")); err != nil { + return errors.Wrap(err, "ocisfs: could not set empty owner id attribute") + } + if err = xattr.Set(nodePath, ownerIDPAttr, []byte("")); err != nil { + return errors.Wrap(err, "ocisfs: could not set empty owner idp attribute") + } + } else { if err = xattr.Set(nodePath, ownerIDAttr, []byte(owner.OpaqueId)); err != nil { return errors.Wrap(err, "ocisfs: could not set owner id attribute") } @@ -67,23 +77,89 @@ func (n *Node) writeMetadata(owner *userpb.UserId) (err error) { return } -// ReadNode creates a new instance from an id and checks if it exists -func ReadNode(ctx context.Context, pw *Path, id string) (n *Node, err error) { +// ReadRecycleItem reads a recycle item as a node +// TODO refactor the returned params into Node properties? would make all the path transformations go away... +func ReadRecycleItem(ctx context.Context, lu *Lookup, key string) (n *Node, trashItem string, deletedNodePath string, origin string, err error) { + + if key == "" { + return nil, "", "", "", errtypes.InternalError("key is empty") + } + + kp := strings.SplitN(key, ":", 2) + if len(kp) != 2 { + appctx.GetLogger(ctx).Error().Err(err).Str("key", key).Msg("malformed key") + return + } + trashItem = filepath.Join(lu.Options.Root, "trash", kp[0], kp[1]) + + var link string + link, err = os.Readlink(trashItem) + if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Str("trashItem", trashItem).Msg("error reading trash link") + return + } + parts := strings.SplitN(filepath.Base(link), ".T.", 2) + if len(parts) != 2 { + appctx.GetLogger(ctx).Error().Err(err).Str("trashItem", trashItem).Interface("parts", parts).Msg("malformed trash link") + return + } + n = &Node{ - pw: pw, - ID: id, + lu: lu, + ID: parts[0], } - nodePath := filepath.Join(n.pw.Root, "nodes", n.ID) + deletedNodePath = lu.toInternalPath(filepath.Base(link)) // lookup parent id in extended attributes var attrBytes []byte - if attrBytes, err = xattr.Get(nodePath, parentidAttr); err == nil { + if attrBytes, err = xattr.Get(deletedNodePath, parentidAttr); err == nil { n.ParentID = string(attrBytes) } else { return } // lookup name in extended attributes + if attrBytes, err = xattr.Get(deletedNodePath, nameAttr); err == nil { + n.Name = string(attrBytes) + } else { + return + } + + // get origin node + origin = "/" + + // lookup origin path in extended attributes + if attrBytes, err = xattr.Get(deletedNodePath, trashOriginAttr); err == nil { + origin = string(attrBytes) + } else { + log.Error().Err(err).Str("trashItem", trashItem).Str("link", link).Str("deletedNodePath", deletedNodePath).Msg("could not read origin path, restoring to /") + } + return +} + +// ReadNode creates a new instance from an id and checks if it exists +func ReadNode(ctx context.Context, lu *Lookup, id string) (n *Node, err error) { + n = &Node{ + lu: lu, + ID: id, + } + + nodePath := lu.toInternalPath(n.ID) + + // lookup parent id in extended attributes + var attrBytes []byte + attrBytes, err = xattr.Get(nodePath, parentidAttr) + switch { + case err == nil: + n.ParentID = string(attrBytes) + case isNoData(err): + return nil, errtypes.InternalError(err.Error()) + case isNotFound(err): + return n, nil // swallow not found, the node defaults to exists = false + default: + return nil, errtypes.InternalError(err.Error()) + } + // lookup name in extended attributes if attrBytes, err = xattr.Get(nodePath, nameAttr); err == nil { n.Name = string(attrBytes) } else { @@ -91,7 +167,7 @@ func ReadNode(ctx context.Context, pw *Path, id string) (n *Node, err error) { } var root *Node - if root, err = pw.HomeOrRootNode(ctx); err != nil { + if root, err = lu.HomeOrRootNode(ctx); err != nil { return } parentID := n.ParentID @@ -100,14 +176,13 @@ func ReadNode(ctx context.Context, pw *Path, id string) (n *Node, err error) { for parentID != root.ID { log.Debug().Interface("node", n).Str("root.ID", root.ID).Msg("ReadNode()") // walk to root to check node is not part of a deleted subtree - parentPath := filepath.Join(n.pw.Root, "nodes", parentID) - if attrBytes, err = xattr.Get(parentPath, parentidAttr); err == nil { + if attrBytes, err = xattr.Get(lu.toInternalPath(parentID), parentidAttr); err == nil { parentID = string(attrBytes) log.Debug().Interface("node", n).Str("root.ID", root.ID).Str("parentID", parentID).Msg("ReadNode() found parent") } else { log.Error().Err(err).Interface("node", n).Str("root.ID", root.ID).Msg("ReadNode()") - if os.IsNotExist(err) { + if isNotFound(err) { return } return @@ -123,12 +198,12 @@ func ReadNode(ctx context.Context, pw *Path, id string) (n *Node, err error) { // Child returns the child node with the given name func (n *Node) Child(name string) (c *Node, err error) { c = &Node{ - pw: n.pw, + lu: n.lu, ParentID: n.ID, Name: name, } var link string - if link, err = os.Readlink(filepath.Join(n.pw.Root, "nodes", n.ID, name)); os.IsNotExist(err) { + if link, err = os.Readlink(filepath.Join(n.lu.toInternalPath(n.ID), name)); os.IsNotExist(err) { err = nil // if the file does not exist we return a node that has Exists = false return } @@ -151,11 +226,11 @@ func (n *Node) Parent() (p *Node, err error) { return nil, fmt.Errorf("ocisfs: root has no parent") } p = &Node{ - pw: n.pw, + lu: n.lu, ID: n.ParentID, } - parentPath := filepath.Join(n.pw.Root, "nodes", n.ParentID) + parentPath := n.lu.toInternalPath(n.ParentID) // lookup parent id in extended attributes var attrBytes []byte @@ -185,7 +260,7 @@ func (n *Node) Owner() (id string, idp string, err error) { return n.ownerID, n.ownerIDP, nil } - nodePath := filepath.Join(n.pw.Root, "nodes", n.ParentID) + nodePath := n.lu.toInternalPath(n.ID) // lookup parent id in extended attributes var attrBytes []byte // lookup name in extended attributes @@ -208,7 +283,7 @@ func (n *Node) AsResourceInfo(ctx context.Context) (ri *provider.ResourceInfo, e log := appctx.GetLogger(ctx) var fn string - nodePath := filepath.Join(n.pw.Root, "nodes", n.ID) + nodePath := n.lu.toInternalPath(n.ID) var fi os.FileInfo @@ -235,7 +310,7 @@ func (n *Node) AsResourceInfo(ctx context.Context) (ri *provider.ResourceInfo, e id := &provider.ResourceId{OpaqueId: n.ID} - fn, err = n.pw.Path(ctx, n) + fn, err = n.lu.Path(ctx, n) if err != nil { return nil, err } @@ -321,8 +396,7 @@ func (n *Node) AsResourceInfo(ctx context.Context) (ri *provider.ResourceInfo, e // HasPropagation checks if the propagation attribute exists and is set to "1" func (n *Node) HasPropagation() (propagation bool) { - nodePath := filepath.Join(n.pw.Root, "nodes", n.ID) - if b, err := xattr.Get(nodePath, propagationAttr); err == nil { + if b, err := xattr.Get(n.lu.toInternalPath(n.ID), propagationAttr); err == nil { return string(b) == "1" } return false @@ -330,9 +404,8 @@ func (n *Node) HasPropagation() (propagation bool) { // GetTMTime reads the tmtime from the extended attributes func (n *Node) GetTMTime() (tmTime time.Time, err error) { - nodePath := filepath.Join(n.pw.Root, "nodes", n.ID) var b []byte - if b, err = xattr.Get(nodePath, treeMTimeAttr); err != nil { + if b, err = xattr.Get(n.lu.toInternalPath(n.ID), treeMTimeAttr); err != nil { return } return time.Parse(time.RFC3339Nano, string(b)) @@ -340,17 +413,45 @@ func (n *Node) GetTMTime() (tmTime time.Time, err error) { // SetTMTime writes the tmtime to the extended attributes func (n *Node) SetTMTime(t time.Time) (err error) { - nodePath := filepath.Join(n.pw.Root, "nodes", n.ID) - return xattr.Set(nodePath, treeMTimeAttr, []byte(t.UTC().Format(time.RFC3339Nano))) + return xattr.Set(n.lu.toInternalPath(n.ID), treeMTimeAttr, []byte(t.UTC().Format(time.RFC3339Nano))) } // UnsetTempEtag removes the temporary etag attribute func (n *Node) UnsetTempEtag() (err error) { - nodePath := filepath.Join(n.pw.Root, "nodes", n.ID) - if err = xattr.Remove(nodePath, tmpEtagAttr); err != nil { + if err = xattr.Remove(n.lu.toInternalPath(n.ID), tmpEtagAttr); err != nil { if e, ok := err.(*xattr.Error); ok && e.Err.Error() == "no data available" { return nil } } return err } + +// ListGrantees lists the grantees of the current node +// We don't want to wast time and memory by creating grantee objects. +// The function will return a list of opaque strings that can be used to make a ReadGrant call +func (n *Node) ListGrantees(ctx context.Context) (grantees []string, err error) { + var attrs []string + if attrs, err = xattr.List(n.lu.toInternalPath(n.ID)); err != nil { + appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Msg("error listing attributes") + return nil, err + } + for i := range attrs { + if strings.HasPrefix(attrs[i], grantPrefix) { + grantees = append(grantees, attrs[i]) + } + } + return +} + +// ReadGrant reads a CS3 grant +func (n *Node) ReadGrant(ctx context.Context, grantee string) (g *provider.Grant, err error) { + var b []byte + if b, err = xattr.Get(n.lu.toInternalPath(n.ID), grantee); err != nil { + return nil, err + } + var e *ace.ACE + if e, err = ace.Unmarshal(strings.TrimPrefix(grantee, grantPrefix), b); err != nil { + return nil, err + } + return e.Grant(), nil +} diff --git a/pkg/storage/fs/ocis/ocis.go b/pkg/storage/fs/ocis/ocis.go index e19a22115b..8d41907cb8 100644 --- a/pkg/storage/fs/ocis/ocis.go +++ b/pkg/storage/fs/ocis/ocis.go @@ -56,8 +56,8 @@ const ( // updated when the file is renamed or moved nameAttr string = ocisPrefix + "name" - // SharePrefix is the prefix for sharing related extended attributes - sharePrefix string = ocisPrefix + "acl." + // grantPrefix is the prefix for sharing related extended attributes + grantPrefix string = ocisPrefix + "grant." metadataPrefix string = ocisPrefix + "md." // TODO implement favorites metadata flag //favPrefix string = ocisPrefix + "fav." // favorite flag, per user @@ -87,47 +87,47 @@ func init() { registry.Register("ocis", New) } -func parseConfig(m map[string]interface{}) (*Path, error) { - pw := &Path{} - if err := mapstructure.Decode(m, pw); err != nil { +func parseConfig(m map[string]interface{}) (*Options, error) { + o := &Options{} + if err := mapstructure.Decode(m, o); err != nil { err = errors.Wrap(err, "error decoding conf") return nil, err } - return pw, nil + return o, nil } -func (pw *Path) init(m map[string]interface{}) { - if pw.UserLayout == "" { - pw.UserLayout = "{{.Id.OpaqueId}}" +func (o *Options) init(m map[string]interface{}) { + if o.UserLayout == "" { + o.UserLayout = "{{.Id.OpaqueId}}" } // ensure user layout has no starting or trailing / - pw.UserLayout = strings.Trim(pw.UserLayout, "/") + o.UserLayout = strings.Trim(o.UserLayout, "/") - if pw.ShareFolder == "" { - pw.ShareFolder = "/Shares" + if o.ShareFolder == "" { + o.ShareFolder = "/Shares" } // ensure share folder always starts with slash - pw.ShareFolder = filepath.Join("/", pw.ShareFolder) + o.ShareFolder = filepath.Join("/", o.ShareFolder) // c.DataDirectory should never end in / unless it is the root - pw.Root = filepath.Clean(pw.Root) + o.Root = filepath.Clean(o.Root) } // New returns an implementation to of the storage.FS interface that talk to // a local filesystem. func New(m map[string]interface{}) (storage.FS, error) { - pw, err := parseConfig(m) + o, err := parseConfig(m) if err != nil { return nil, err } - pw.init(m) + o.init(m) dataPaths := []string{ - filepath.Join(pw.Root, "nodes"), + filepath.Join(o.Root, "nodes"), // notes contain symlinks from nodes//uploads/ to ../../uploads/ // better to keep uploads on a fast / volatile storage before a workflow finally moves them to the nodes dir - filepath.Join(pw.Root, "uploads"), - filepath.Join(pw.Root, "trash"), + filepath.Join(o.Root, "uploads"), + filepath.Join(o.Root, "trash"), } for _, v := range dataPaths { if err := os.MkdirAll(v, 0700); err != nil { @@ -137,26 +137,34 @@ func New(m map[string]interface{}) (storage.FS, error) { } } + lu := &Lookup{ + Options: o, + } + // the root node has an empty name, or use `.` ? // the root node has no parent, or use `root` ? - if err = createNode(&Node{pw: pw, ID: "root"}, nil); err != nil { + if err = createNode(&Node{lu: lu, ID: "root"}, nil); err != nil { return nil, err } - tp, err := NewTree(pw) + tp, err := NewTree(lu) if err != nil { return nil, err } return &ocisfs{ tp: tp, - pw: pw, + lu: lu, + o: o, + p: &Permissions{lu: lu}, }, nil } type ocisfs struct { tp TreePersistence - pw *Path + lu *Lookup + o *Options + p *Permissions } func (fs *ocisfs) Shutdown(ctx context.Context) error { @@ -169,15 +177,15 @@ func (fs *ocisfs) GetQuota(ctx context.Context) (int, int, error) { // CreateHome creates a new root node that has no parent id func (fs *ocisfs) CreateHome(ctx context.Context) (err error) { - if !fs.pw.EnableHome || fs.pw.UserLayout == "" { + if !fs.o.EnableHome || fs.o.UserLayout == "" { return errtypes.NotSupported("ocisfs: CreateHome() home supported disabled") } var n, h *Node - if n, err = fs.pw.RootNode(ctx); err != nil { + if n, err = fs.lu.RootNode(ctx); err != nil { return } - h, err = fs.pw.WalkPath(ctx, n, fs.pw.mustGetUserLayout(ctx), func(ctx context.Context, n *Node) error { + h, err = fs.lu.WalkPath(ctx, n, fs.lu.mustGetUserLayout(ctx), func(ctx context.Context, n *Node) error { if !n.Exists { if err := fs.tp.CreateDir(ctx, n); err != nil { return err @@ -186,8 +194,8 @@ func (fs *ocisfs) CreateHome(ctx context.Context) (err error) { return nil }) - if fs.pw.TreeTimeAccounting { - homePath := filepath.Join(fs.pw.Root, "nodes", h.ID) + if fs.o.TreeTimeAccounting { + homePath := h.lu.toInternalPath(h.ID) // mark the home node as the end of propagation if err = xattr.Set(homePath, propagationAttr, []byte("1")); err != nil { appctx.GetLogger(ctx).Error().Err(err).Interface("node", h).Msg("could not mark home as propagation root") @@ -200,12 +208,12 @@ func (fs *ocisfs) CreateHome(ctx context.Context) (err error) { // GetHome is called to look up the home path for a user // It is NOT supposed to return the internal path but the external path func (fs *ocisfs) GetHome(ctx context.Context) (string, error) { - if !fs.pw.EnableHome || fs.pw.UserLayout == "" { + if !fs.o.EnableHome || fs.o.UserLayout == "" { return "", errtypes.NotSupported("ocisfs: GetHome() home supported disabled") } u := user.ContextMustGetUser(ctx) - layout := templates.WithUser(u, fs.pw.UserLayout) - return filepath.Join(fs.pw.Root, layout), nil // TODO use a namespace? + layout := templates.WithUser(u, fs.o.UserLayout) + return filepath.Join(fs.o.Root, layout), nil // TODO use a namespace? } // Tree persistence @@ -216,20 +224,36 @@ func (fs *ocisfs) GetPathByID(ctx context.Context, id *provider.ResourceId) (str } func (fs *ocisfs) CreateDir(ctx context.Context, fn string) (err error) { - var node *Node - if node, err = fs.pw.NodeFromPath(ctx, fn); err != nil { + var n *Node + if n, err = fs.lu.NodeFromPath(ctx, fn); err != nil { return } - if node.Exists { + + if n.Exists { return errtypes.AlreadyExists(fn) } - err = fs.tp.CreateDir(ctx, node) - if fs.pw.TreeTimeAccounting { - nodePath := filepath.Join(fs.pw.Root, "nodes", node.ID) + pn, err := n.Parent() + if err != nil { + return errors.Wrap(err, "ocisfs: error getting parent "+n.ParentID) + } + ok, err := fs.p.HasPermission(ctx, pn, func(rp *provider.ResourcePermissions) bool { + return rp.CreateContainer + }) + switch { + case err != nil: + return errtypes.InternalError(err.Error()) + case !ok: + return errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name)) + } + + err = fs.tp.CreateDir(ctx, n) + + if fs.o.TreeTimeAccounting { + nodePath := n.lu.toInternalPath(n.ID) // mark the home node as the end of propagation if err = xattr.Set(nodePath, propagationAttr, []byte("1")); err != nil { - appctx.GetLogger(ctx).Error().Err(err).Interface("node", node).Msg("could not mark node to propagate") + appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Msg("could not mark node to propagate") return } } @@ -247,16 +271,16 @@ func (fs *ocisfs) CreateReference(ctx context.Context, p string, targetURI *url. parts := strings.Split(p, "/") if len(parts) != 2 { - return errtypes.PermissionDenied("ocisfs: references must be a child of the share folder: share_folder=" + fs.pw.ShareFolder + " path=" + p) + return errtypes.PermissionDenied("ocisfs: references must be a child of the share folder: share_folder=" + fs.o.ShareFolder + " path=" + p) } - if parts[0] != strings.Trim(fs.pw.ShareFolder, "/") { - return errtypes.PermissionDenied("ocisfs: cannot create references outside the share folder: share_folder=" + fs.pw.ShareFolder + " path=" + p) + if parts[0] != strings.Trim(fs.o.ShareFolder, "/") { + return errtypes.PermissionDenied("ocisfs: cannot create references outside the share folder: share_folder=" + fs.o.ShareFolder + " path=" + p) } // create Shares folder if it does not exist var n *Node - if n, err = fs.pw.NodeFromPath(ctx, fs.pw.ShareFolder); err != nil { + if n, err = fs.lu.NodeFromPath(ctx, fs.o.ShareFolder); err != nil { return errtypes.InternalError(err.Error()) } else if !n.Exists { if err = fs.tp.CreateDir(ctx, n); err != nil { @@ -277,7 +301,7 @@ func (fs *ocisfs) CreateReference(ctx context.Context, p string, targetURI *url. return } - internal := filepath.Join(fs.pw.Root, "nodes", n.ID) + internal := n.lu.toInternalPath(n.ID) if err = xattr.Set(internal, referenceAttr, []byte(targetURI.String())); err != nil { return errors.Wrapf(err, "ocisfs: error setting the target %s on the reference file %s", targetURI.String(), internal) } @@ -286,41 +310,81 @@ func (fs *ocisfs) CreateReference(ctx context.Context, p string, targetURI *url. func (fs *ocisfs) Move(ctx context.Context, oldRef, newRef *provider.Reference) (err error) { var oldNode, newNode *Node - if oldNode, err = fs.pw.NodeFromResource(ctx, oldRef); err != nil { + if oldNode, err = fs.lu.NodeFromResource(ctx, oldRef); err != nil { return } + if !oldNode.Exists { err = errtypes.NotFound(filepath.Join(oldNode.ParentID, oldNode.Name)) return } - if newNode, err = fs.pw.NodeFromResource(ctx, newRef); err != nil { + ok, err := fs.p.HasPermission(ctx, oldNode, func(rp *provider.ResourcePermissions) bool { + return rp.Move + }) + switch { + case err != nil: + return errtypes.InternalError(err.Error()) + case !ok: + return errtypes.PermissionDenied(oldNode.ID) + } + + if newNode, err = fs.lu.NodeFromResource(ctx, newRef); err != nil { + return + } + if newNode.Exists { + err = errtypes.AlreadyExists(filepath.Join(newNode.ParentID, newNode.Name)) return } + return fs.tp.Move(ctx, oldNode, newNode) } func (fs *ocisfs) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []string) (ri *provider.ResourceInfo, err error) { var node *Node - if node, err = fs.pw.NodeFromResource(ctx, ref); err != nil { + if node, err = fs.lu.NodeFromResource(ctx, ref); err != nil { return } + if !node.Exists { err = errtypes.NotFound(filepath.Join(node.ParentID, node.Name)) return } + + ok, err := fs.p.HasPermission(ctx, node, func(rp *provider.ResourcePermissions) bool { + return rp.Stat + }) + switch { + case err != nil: + return nil, errtypes.InternalError(err.Error()) + case !ok: + return nil, errtypes.PermissionDenied(node.ID) + } + return node.AsResourceInfo(ctx) } func (fs *ocisfs) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys []string) (finfos []*provider.ResourceInfo, err error) { var node *Node - if node, err = fs.pw.NodeFromResource(ctx, ref); err != nil { + if node, err = fs.lu.NodeFromResource(ctx, ref); err != nil { return } + if !node.Exists { err = errtypes.NotFound(filepath.Join(node.ParentID, node.Name)) return } + + ok, err := fs.p.HasPermission(ctx, node, func(rp *provider.ResourcePermissions) bool { + return rp.ListContainer + }) + switch { + case err != nil: + return nil, errtypes.InternalError(err.Error()) + case !ok: + return nil, errtypes.PermissionDenied(node.ID) + } + var children []*Node children, err = fs.tp.ListFolder(ctx, node) if err != nil { @@ -337,24 +401,35 @@ func (fs *ocisfs) ListFolder(ctx context.Context, ref *provider.Reference, mdKey func (fs *ocisfs) Delete(ctx context.Context, ref *provider.Reference) (err error) { var node *Node - if node, err = fs.pw.NodeFromResource(ctx, ref); err != nil { + if node, err = fs.lu.NodeFromResource(ctx, ref); err != nil { return } if !node.Exists { err = errtypes.NotFound(filepath.Join(node.ParentID, node.Name)) return } + + ok, err := fs.p.HasPermission(ctx, node, func(rp *provider.ResourcePermissions) bool { + return rp.Delete + }) + switch { + case err != nil: + return errtypes.InternalError(err.Error()) + case !ok: + return errtypes.PermissionDenied(filepath.Join(node.ParentID, node.Name)) + } + return fs.tp.Delete(ctx, node) } // Data persistence -func (fs *ocisfs) ContentPath(node *Node) string { - return filepath.Join(fs.pw.Root, "nodes", node.ID) +func (fs *ocisfs) ContentPath(n *Node) string { + return n.lu.toInternalPath(n.ID) } func (fs *ocisfs) Download(ctx context.Context, ref *provider.Reference) (io.ReadCloser, error) { - node, err := fs.pw.NodeFromResource(ctx, ref) + node, err := fs.lu.NodeFromResource(ctx, ref) if err != nil { return nil, errors.Wrap(err, "ocisfs: error resolving ref") } @@ -364,6 +439,16 @@ func (fs *ocisfs) Download(ctx context.Context, ref *provider.Reference) (io.Rea return nil, err } + ok, err := fs.p.HasPermission(ctx, node, func(rp *provider.ResourcePermissions) bool { + return rp.InitiateFileDownload + }) + switch { + case err != nil: + return nil, errtypes.InternalError(err.Error()) + case !ok: + return nil, errtypes.PermissionDenied(filepath.Join(node.ParentID, node.Name)) + } + contentPath := fs.ContentPath(node) r, err := os.Open(contentPath) diff --git a/pkg/storage/fs/ocis/option.go b/pkg/storage/fs/ocis/option.go new file mode 100644 index 0000000000..e8ee62464a --- /dev/null +++ b/pkg/storage/fs/ocis/option.go @@ -0,0 +1,98 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package ocis + +// Option defines a single option function. +type Option func(o *Options) + +// Options defines the available options for this package. +type Options struct { + // ocis fs works on top of a dir of uuid nodes + Root string `mapstructure:"root"` + + // UserLayout describes the relative path from the storage's root node to the users home node. + UserLayout string `mapstructure:"user_layout"` + + // TODO NodeLayout option to save nodes as eg. nodes/1d/d8/1dd84abf-9466-4e14-bb86-02fc4ea3abcf + ShareFolder string `mapstructure:"share_folder"` + + // EnableHome enables the creation of home directories. + EnableHome bool `mapstructure:"enable_home"` + + // propagate mtime changes as tmtime (tree modification time) to the parent directory when user.ocis.propagation=1 is set on a node + TreeTimeAccounting bool `mapstructure:"treetime_accounting"` + + // propagate size changes as treesize + TreeSizeAccounting bool `mapstructure:"treesize_accounting"` +} + +// newOptions initializes the available default options. +/* for future use, commented to make linter happy +func newOptions(opts ...Option) Options { + opt := Options{} + + for _, o := range opts { + o(&opt) + } + + return opt +} +*/ + +// Root provides a function to set the root option. +func Root(val string) Option { + return func(o *Options) { + o.Root = val + } +} + +// UserLayout provides a function to set the user layout option. +func UserLayout(val string) Option { + return func(o *Options) { + o.UserLayout = val + } +} + +// ShareFolder provides a function to set the ShareFolder option. +func ShareFolder(val string) Option { + return func(o *Options) { + o.ShareFolder = val + } +} + +// EnableHome provides a function to set the EnableHome option. +func EnableHome(val bool) Option { + return func(o *Options) { + o.EnableHome = val + } +} + +// TreeTimeAccounting provides a function to set the TreeTimeAccounting option. +func TreeTimeAccounting(val bool) Option { + return func(o *Options) { + o.TreeTimeAccounting = val + } +} + +// TreeSizeAccounting provides a function to set the TreeSizeAccounting option. +func TreeSizeAccounting(val bool) Option { + return func(o *Options) { + o.TreeSizeAccounting = val + } +} diff --git a/pkg/storage/fs/ocis/permissions.go b/pkg/storage/fs/ocis/permissions.go new file mode 100644 index 0000000000..6dcfd9c4e3 --- /dev/null +++ b/pkg/storage/fs/ocis/permissions.go @@ -0,0 +1,187 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package ocis + +import ( + "context" + "strings" + "syscall" + + userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/user" + "github.com/pkg/errors" + "github.com/pkg/xattr" +) + +var defaultPermissions *provider.ResourcePermissions = &provider.ResourcePermissions{ + // no permissions +} + +// permissions for nodes that don't have an owner set, eg the root node +var noOwnerPermissions *provider.ResourcePermissions = &provider.ResourcePermissions{ + Stat: true, +} +var ownerPermissions *provider.ResourcePermissions = &provider.ResourcePermissions{ + // all permissions + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + InitiateFileUpload: true, + ListContainer: true, + ListFileVersions: true, + ListGrants: true, + ListRecycle: true, + Move: true, + PurgeRecycle: true, + RemoveGrant: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + Stat: true, + UpdateGrant: true, +} + +// Permissions implements permission checks +type Permissions struct { + lu *Lookup +} + +// HasPermission call check() for every node up to the root until check returns true +func (p *Permissions) HasPermission(ctx context.Context, n *Node, check func(*provider.ResourcePermissions) bool) (can bool, err error) { + + var u *userv1beta1.User + var perms *provider.ResourcePermissions + if u, perms = p.getUserAndPermissions(ctx, n); perms != nil { + return check(perms), nil + } + + // determine root + var rn *Node + if rn, err = p.lu.RootNode(ctx); err != nil { + return false, err + } + + cn := n + + // for an efficient group lookup convert the list of groups to a map + // groups are just strings ... groupnames ... or group ids ??? AAARGH !!! + groupsMap := make(map[string]bool, len(u.Groups)) + for i := range u.Groups { + groupsMap[u.Groups[i]] = true + } + + var g *provider.Grant + // for all segments, starting at the leaf + for cn.ID != rn.ID { + + var grantees []string + if grantees, err = n.ListGrantees(ctx); err != nil { + appctx.GetLogger(ctx).Error().Err(err).Interface("node", cn).Msg("error listing grantees") + return false, err + } + + userace := grantPrefix + "u:" + u.Id.OpaqueId + userFound := false + for i := range grantees { + // we only need the find the user once per node + switch { + case !userFound && grantees[i] == userace: + g, err = n.ReadGrant(ctx, grantees[i]) + case strings.HasPrefix(grantees[i], grantPrefix+"g:"): + gr := strings.TrimPrefix(grantees[i], grantPrefix+"g:") + if groupsMap[gr] { + g, err = n.ReadGrant(ctx, grantees[i]) + } else { + // no need to check attribute + continue + } + default: + // no need to check attribute + continue + } + + switch { + case err == nil: + appctx.GetLogger(ctx).Debug().Interface("node", cn).Str("grant", grantees[i]).Interface("permissions", g.GetPermissions()).Msg("checking permissions") + if check(g.GetPermissions()) { + return true, nil + } + case isNoData(err): + err = nil + appctx.GetLogger(ctx).Error().Interface("node", cn).Str("grant", grantees[i]).Interface("grantees", grantees).Msg("grant vanished from node after listing") + default: + appctx.GetLogger(ctx).Error().Err(err).Interface("node", cn).Str("grant", grantees[i]).Msg("error reading permissions") + return false, err + } + } + + if cn, err = cn.Parent(); err != nil { + return false, errors.Wrap(err, "ocisfs: error getting parent "+cn.ParentID) + } + } + + appctx.GetLogger(ctx).Debug().Interface("permissions", defaultPermissions).Interface("node", n).Interface("user", u).Msg("no grant found, returning default permissions") + return false, nil +} + +func (p *Permissions) getUserAndPermissions(ctx context.Context, n *Node) (*userv1beta1.User, *provider.ResourcePermissions) { + u, ok := user.ContextGetUser(ctx) + if !ok { + appctx.GetLogger(ctx).Debug().Interface("node", n).Msg("no user in context, returning default permissions") + return nil, defaultPermissions + } + // check if the current user is the owner + id, _, err := n.Owner() + if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Msg("could not determine owner, returning default permissions") + return nil, defaultPermissions + } + if id == "" { + // TODO what if no owner is set but grants are present? + return nil, noOwnerPermissions + } + if id == u.Id.OpaqueId { + appctx.GetLogger(ctx).Debug().Interface("node", n).Msg("user is owner, returning owner permissions") + return u, ownerPermissions + } + return u, nil +} +func isNoData(err error) bool { + if xerr, ok := err.(*xattr.Error); ok { + if serr, ok2 := xerr.Err.(syscall.Errno); ok2 { + return serr == syscall.ENODATA + } + } + return false +} + +// The os not exists error is buried inside the xattr error, +// so we cannot just use os.IsNotExists(). +func isNotFound(err error) bool { + if xerr, ok := err.(*xattr.Error); ok { + if serr, ok2 := xerr.Err.(syscall.Errno); ok2 { + return serr == syscall.ENOENT + } + } + return false +} diff --git a/pkg/storage/fs/ocis/recycle.go b/pkg/storage/fs/ocis/recycle.go index 359561a408..31022e5fd9 100644 --- a/pkg/storage/fs/ocis/recycle.go +++ b/pkg/storage/fs/ocis/recycle.go @@ -49,6 +49,19 @@ func (fs *ocisfs) ListRecycle(ctx context.Context) (items []*provider.RecycleIte items = make([]*provider.RecycleItem, 0) + // TODO how do we check if the storage allows listing the recycle for the current user? check owner of the root of the storage? + if fs.o.EnableHome { + if !ownerPermissions.ListContainer { + log.Debug().Msg("owner not allowed to list trash") + return items, errtypes.PermissionDenied("owner not allowed to list trash") + } + } else { + if !defaultPermissions.ListContainer { + log.Debug().Msg("default permissions prevent listing trash") + return items, errtypes.PermissionDenied("default permissions prevent listing trash") + } + } + f, err := os.Open(trashRoot) if err != nil { if os.IsNotExist(err) { @@ -62,25 +75,26 @@ func (fs *ocisfs) ListRecycle(ctx context.Context) (items []*provider.RecycleIte return nil, err } for i := range names { - var link string - link, err = os.Readlink(filepath.Join(trashRoot, names[i])) + var trashnode string + trashnode, err = os.Readlink(filepath.Join(trashRoot, names[i])) if err != nil { log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Msg("error reading trash link, skipping") err = nil continue } - parts := strings.SplitN(filepath.Base(link), ".T.", 2) + parts := strings.SplitN(filepath.Base(trashnode), ".T.", 2) if len(parts) != 2 { - log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("link", link).Interface("parts", parts).Msg("malformed trash link, skipping") + log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("trashnode", trashnode).Interface("parts", parts).Msg("malformed trash link, skipping") continue } - nodePath := filepath.Join(fs.pw.Root, "nodes", filepath.Base(link)) + nodePath := fs.lu.toInternalPath(filepath.Base(trashnode)) md, err := os.Stat(nodePath) if err != nil { - log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("link", link).Interface("parts", parts).Msg("could not stat trash item, skipping") + log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("trashnode", trashnode).Interface("parts", parts).Msg("could not stat trash item, skipping") continue } + item := &provider.RecycleItem{ Type: getResourceType(md.IsDir()), Size: uint64(md.Size()), @@ -92,15 +106,32 @@ func (fs *ocisfs) ListRecycle(ctx context.Context) (items []*provider.RecycleIte // TODO nanos } } else { - log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("link", link).Interface("parts", parts).Msg("could parse time format, ignoring") + log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("link", trashnode).Interface("parts", parts).Msg("could parse time format, ignoring") } - // lookup parent id in extended attributes + // lookup origin path in extended attributes var attrBytes []byte if attrBytes, err = xattr.Get(nodePath, trashOriginAttr); err == nil { item.Path = string(attrBytes) } else { - log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("link", link).Msg("could not read origin path, skipping") + log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("link", trashnode).Msg("could not read origin path, skipping") + continue + } + // TODO filter results by permission ... on the original parent? or the trashed node? + // if it were on the original parent it would be possible to see files that were trashed before the current user got access + // so -> check the trash node itself + // hmm listing trash currently lists the current users trash or the 'root' trash. from ocs only the home storage is queried for trash items. + // for now we can only really check if the current user is the owner + if attrBytes, err = xattr.Get(nodePath, ownerIDAttr); err == nil { + if fs.o.EnableHome { + u := user.ContextMustGetUser(ctx) + if u.Id.OpaqueId != string(attrBytes) { + log.Warn().Str("trashRoot", trashRoot).Str("name", names[i]).Str("link", trashnode).Msg("trash item not owned by current user, skipping") + continue + } + } + } else { + log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("link", trashnode).Msg("could not read owner, skipping") continue } @@ -112,45 +143,28 @@ func (fs *ocisfs) ListRecycle(ctx context.Context) (items []*provider.RecycleIte func (fs *ocisfs) RestoreRecycleItem(ctx context.Context, key string) (err error) { log := appctx.GetLogger(ctx) - if key == "" { - return errtypes.InternalError("key is empty") - } - - kp := strings.SplitN(key, ":", 2) - if len(kp) != 2 { - log.Error().Err(err).Str("key", key).Msg("malformed key") + var rn *Node + var trashItem string + var deletedNodePath string + var origin string + if rn, trashItem, deletedNodePath, origin, err = ReadRecycleItem(ctx, fs.lu, key); err != nil { return } - trashItem := filepath.Join(fs.pw.Root, "trash", kp[0], kp[1]) - var link string - link, err = os.Readlink(trashItem) - if err != nil { - log.Error().Err(err).Str("trashItem", trashItem).Msg("error reading trash link") - return - } - parts := strings.SplitN(filepath.Base(link), ".T.", 2) - if len(parts) != 2 { - log.Error().Err(err).Str("trashItem", trashItem).Interface("parts", parts).Msg("malformed trash link") - return - } - - deletedNodePath := filepath.Join(fs.pw.Root, "nodes", filepath.Base(link)) - - // get origin node - origin := "/" - - // lookup parent id in extended attributes - var attrBytes []byte - if attrBytes, err = xattr.Get(deletedNodePath, trashOriginAttr); err == nil { - origin = string(attrBytes) - } else { - log.Error().Err(err).Str("trashItem", trashItem).Str("link", link).Str("deletedNodePath", deletedNodePath).Msg("could not read origin path, restoring to /") + // check permissions of deleted node + ok, err := fs.p.HasPermission(ctx, rn, func(rp *provider.ResourcePermissions) bool { + return rp.RestoreRecycleItem + }) + switch { + case err != nil: + return errtypes.InternalError(err.Error()) + case !ok: + return errtypes.PermissionDenied(key) } // link to origin var n *Node - n, err = fs.pw.NodeFromPath(ctx, origin) + n, err = fs.lu.NodeFromPath(ctx, origin) if err != nil { return } @@ -159,18 +173,19 @@ func (fs *ocisfs) RestoreRecycleItem(ctx context.Context, key string) (err error return errtypes.AlreadyExists("origin already exists") } - // rename to node only name, so it is picked up by id - nodePath := filepath.Join(fs.pw.Root, "nodes", parts[0]) - err = os.Rename(deletedNodePath, nodePath) + // add the entry for the parent dir + err = os.Symlink("../"+rn.ID, filepath.Join(fs.lu.toInternalPath(n.ParentID), n.Name)) if err != nil { return } - // add the entry for the parent dir - err = os.Symlink("../"+parts[0], filepath.Join(fs.pw.Root, "nodes", n.ParentID, n.Name)) + // rename to node only name, so it is picked up by id + nodePath := fs.lu.toInternalPath(rn.ID) + err = os.Rename(deletedNodePath, nodePath) if err != nil { return } + n.Exists = true // delete item link in trash @@ -184,22 +199,24 @@ func (fs *ocisfs) RestoreRecycleItem(ctx context.Context, key string) (err error func (fs *ocisfs) PurgeRecycleItem(ctx context.Context, key string) (err error) { log := appctx.GetLogger(ctx) - kp := strings.SplitN(key, ":", 2) - if len(kp) != 2 { - log.Error().Str("key", key).Msg("malformed key") + var rn *Node + var trashItem string + var deletedNodePath string + if rn, trashItem, deletedNodePath, _, err = ReadRecycleItem(ctx, fs.lu, key); err != nil { return } - trashItem := filepath.Join(fs.pw.Root, "trash", kp[0], kp[1]) - var link string - link, err = os.Readlink(trashItem) - if err != nil { - log.Error().Err(err).Str("trashItem", trashItem).Msg("error reading trash link") - return + // check permissions of deleted node + ok, err := fs.p.HasPermission(ctx, rn, func(rp *provider.ResourcePermissions) bool { + return rp.PurgeRecycle + }) + switch { + case err != nil: + return errtypes.InternalError(err.Error()) + case !ok: + return errtypes.PermissionDenied(key) } - // delete trash node link in nodes dir - deletedNodePath := filepath.Join(fs.pw.Root, "nodes", filepath.Base(link)) if err = os.Remove(deletedNodePath); err != nil { log.Error().Err(err).Str("deletedNodePath", deletedNodePath).Msg("error deleting trash node") return @@ -209,10 +226,18 @@ func (fs *ocisfs) PurgeRecycleItem(ctx context.Context, key string) (err error) if err = os.Remove(trashItem); err != nil { log.Error().Err(err).Str("trashItem", trashItem).Msg("error deleting trash item") } + // TODO recursively delete all children return } func (fs *ocisfs) EmptyRecycle(ctx context.Context) error { + // TODO what permission should we check? we could check the root node of the user? or the owner permissions on his home root node? + // The current impl will wipe your own trash. or when enable home is false the trash of the 'root' + if fs.o.EnableHome { + u := user.ContextMustGetUser(ctx) + // TODO use layout, see Tree.Delete() for problem + return os.RemoveAll(filepath.Join(fs.o.Root, "trash", u.Id.OpaqueId)) + } return os.RemoveAll(fs.getRecycleRoot(ctx)) } @@ -224,10 +249,10 @@ func getResourceType(isDir bool) provider.ResourceType { } func (fs *ocisfs) getRecycleRoot(ctx context.Context) string { - if fs.pw.EnableHome { + if fs.o.EnableHome { u := user.ContextMustGetUser(ctx) // TODO use layout, see Tree.Delete() for problem - return filepath.Join(fs.pw.Root, "trash", u.Id.OpaqueId) + return filepath.Join(fs.o.Root, "trash", u.Id.OpaqueId) } - return filepath.Join(fs.pw.Root, "trash", "root") + return filepath.Join(fs.o.Root, "trash", "root") } diff --git a/pkg/storage/fs/ocis/revisions.go b/pkg/storage/fs/ocis/revisions.go index 50b8164e83..898cd5c348 100644 --- a/pkg/storage/fs/ocis/revisions.go +++ b/pkg/storage/fs/ocis/revisions.go @@ -41,18 +41,28 @@ import ( // and replace the revision file with a symbolic link in the future, if necessary. func (fs *ocisfs) ListRevisions(ctx context.Context, ref *provider.Reference) (revisions []*provider.FileVersion, err error) { - var node *Node - if node, err = fs.pw.NodeFromResource(ctx, ref); err != nil { + var n *Node + if n, err = fs.lu.NodeFromResource(ctx, ref); err != nil { return } - if !node.Exists { - err = errtypes.NotFound(filepath.Join(node.ParentID, node.Name)) + if !n.Exists { + err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name)) return } + ok, err := fs.p.HasPermission(ctx, n, func(rp *provider.ResourcePermissions) bool { + return rp.ListFileVersions + }) + switch { + case err != nil: + return nil, errtypes.InternalError(err.Error()) + case !ok: + return nil, errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name)) + } + revisions = []*provider.FileVersion{} - nodePath := filepath.Join(fs.pw.Root, "nodes", node.ID) - if items, err := filepath.Glob(nodePath + ".REV.*"); err == nil { + np := fs.lu.toInternalPath(n.ID) + if items, err := filepath.Glob(np + ".REV.*"); err == nil { for i := range items { if fi, err := os.Stat(items[i]); err == nil { rev := &provider.FileVersion{ @@ -79,15 +89,27 @@ func (fs *ocisfs) DownloadRevision(ctx context.Context, ref *provider.Reference, log.Debug().Str("revisionKey", revisionKey).Msg("DownloadRevision") // check if the node is available and has not been deleted - nodePath := filepath.Join(fs.pw.Root, "nodes", kp[0]) - if _, err := os.Stat(nodePath); err != nil { - if os.IsNotExist(err) { - return nil, errtypes.NotFound(nodePath) - } - return nil, errors.Wrap(err, "ocisfs: error stating node "+kp[0]) + n, err := ReadNode(ctx, fs.lu, kp[0]) + if err != nil { + return nil, err + } + if !n.Exists { + err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name)) + return nil, err + } + + ok, err := fs.p.HasPermission(ctx, n, func(rp *provider.ResourcePermissions) bool { + // TODO add explicit permission in the CS3 api? + return rp.ListFileVersions && rp.RestoreFileVersion && rp.InitiateFileDownload + }) + switch { + case err != nil: + return nil, errtypes.InternalError(err.Error()) + case !ok: + return nil, errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name)) } - contentPath := filepath.Join(fs.pw.Root, "nodes", revisionKey) + contentPath := fs.lu.toInternalPath(revisionKey) r, err := os.Open(contentPath) if err != nil { @@ -109,12 +131,32 @@ func (fs *ocisfs) RestoreRevision(ctx context.Context, ref *provider.Reference, return errtypes.NotFound(revisionKey) } + // check if the node is available and has not been deleted + n, err := ReadNode(ctx, fs.lu, kp[0]) + if err != nil { + return err + } + if !n.Exists { + err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name)) + return err + } + + ok, err := fs.p.HasPermission(ctx, n, func(rp *provider.ResourcePermissions) bool { + return rp.RestoreFileVersion + }) + switch { + case err != nil: + return errtypes.InternalError(err.Error()) + case !ok: + return errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name)) + } + // move current version to new revision - nodePath := filepath.Join(fs.pw.Root, "nodes", kp[0]) + nodePath := fs.lu.toInternalPath(kp[0]) var fi os.FileInfo if fi, err = os.Stat(nodePath); err == nil { // versions are stored alongside the actual file, so a rename can be efficient and does not cross storage / partition boundaries - versionsPath := filepath.Join(fs.pw.Root, "nodes", kp[0]+".REV."+fi.ModTime().UTC().Format(time.RFC3339Nano)) + versionsPath := fs.lu.toInternalPath(kp[0] + ".REV." + fi.ModTime().UTC().Format(time.RFC3339Nano)) err = os.Rename(nodePath, versionsPath) if err != nil { @@ -123,7 +165,7 @@ func (fs *ocisfs) RestoreRevision(ctx context.Context, ref *provider.Reference, // copy old revision to current location - revisionPath := filepath.Join(fs.pw.Root, "nodes", revisionKey) + revisionPath := fs.lu.toInternalPath(revisionKey) var revision, destination *os.File revision, err = os.Open(revisionPath) if err != nil { @@ -144,6 +186,6 @@ func (fs *ocisfs) RestoreRevision(ctx context.Context, ref *provider.Reference, return fs.copyMD(revisionPath, nodePath) } - log.Error().Err(err).Interface("ref", ref).Str("revisionKey", revisionKey).Msg("original node does not exist") + log.Error().Err(err).Interface("ref", ref).Str("originalnode", kp[0]).Str("revisionKey", revisionKey).Msg("original node does not exist") return } diff --git a/pkg/storage/fs/ocis/tree.go b/pkg/storage/fs/ocis/tree.go index 23d113689b..f58390cabe 100644 --- a/pkg/storage/fs/ocis/tree.go +++ b/pkg/storage/fs/ocis/tree.go @@ -37,19 +37,19 @@ import ( // Tree manages a hierarchical tree type Tree struct { - pw *Path + lu *Lookup } // NewTree creates a new Tree instance -func NewTree(pw *Path) (TreePersistence, error) { +func NewTree(lu *Lookup) (TreePersistence, error) { return &Tree{ - pw: pw, + lu: lu, }, nil } // GetMD returns the metadata of a node in the tree func (t *Tree) GetMD(ctx context.Context, node *Node) (os.FileInfo, error) { - md, err := os.Stat(filepath.Join(t.pw.Root, "nodes", node.ID)) + md, err := os.Stat(t.lu.toInternalPath(node.ID)) if err != nil { if os.IsNotExist(err) { return nil, errtypes.NotFound(node.ID) @@ -63,12 +63,12 @@ func (t *Tree) GetMD(ctx context.Context, node *Node) (os.FileInfo, error) { // GetPathByID returns the fn pointed by the file id, without the internal namespace func (t *Tree) GetPathByID(ctx context.Context, id *provider.ResourceId) (relativeExternalPath string, err error) { var node *Node - node, err = t.pw.NodeFromID(ctx, id) + node, err = t.lu.NodeFromID(ctx, id) if err != nil { return } - relativeExternalPath, err = t.pw.Path(ctx, node) + relativeExternalPath, err = t.lu.Path(ctx, node) return } @@ -76,7 +76,7 @@ func (t *Tree) GetPathByID(ctx context.Context, id *provider.ResourceId) (relati // TODO check if node exists? func createNode(n *Node, owner *userpb.UserId) (err error) { // create a directory node - nodePath := filepath.Join(n.pw.Root, "nodes", n.ID) + nodePath := n.lu.toInternalPath(n.ID) if err = os.MkdirAll(nodePath, 0700); err != nil { return errors.Wrap(err, "ocisfs: error creating node") } @@ -94,7 +94,7 @@ func (t *Tree) CreateDir(ctx context.Context, node *Node) (err error) { // create a directory node node.ID = uuid.New().String() - if t.pw.EnableHome { + if t.lu.Options.EnableHome { if u, ok := user.ContextGetUser(ctx); ok { err = createNode(node, u.Id) } else { @@ -110,7 +110,7 @@ func (t *Tree) CreateDir(ctx context.Context, node *Node) (err error) { } // make child appear in listings - err = os.Symlink("../"+node.ID, filepath.Join(t.pw.Root, "nodes", node.ParentID, node.Name)) + err = os.Symlink("../"+node.ID, filepath.Join(t.lu.toInternalPath(node.ParentID), node.Name)) if err != nil { return } @@ -122,14 +122,14 @@ func (t *Tree) Move(ctx context.Context, oldNode *Node, newNode *Node) (err erro // if target exists delete it without trashing it if newNode.Exists { // TODO make sure all children are deleted - if err := os.RemoveAll(filepath.Join(t.pw.Root, "nodes", newNode.ID)); err != nil { + if err := os.RemoveAll(t.lu.toInternalPath(newNode.ID)); err != nil { return errors.Wrap(err, "ocisfs: Move: error deleting target node "+newNode.ID) } } // are we just renaming (parent stays the same)? if oldNode.ParentID == newNode.ParentID { - parentPath := filepath.Join(t.pw.Root, "nodes", oldNode.ParentID) + parentPath := t.lu.toInternalPath(oldNode.ParentID) // rename child err = os.Rename( @@ -141,7 +141,7 @@ func (t *Tree) Move(ctx context.Context, oldNode *Node, newNode *Node) (err erro } // the new node id might be different, so we need to use the old nodes id - tgtPath := filepath.Join(t.pw.Root, "nodes", oldNode.ID) + tgtPath := t.lu.toInternalPath(oldNode.ID) // update name attribute if err := xattr.Set(tgtPath, nameAttr, []byte(newNode.Name)); err != nil { @@ -156,15 +156,15 @@ func (t *Tree) Move(ctx context.Context, oldNode *Node, newNode *Node) (err erro // rename child err = os.Rename( - filepath.Join(t.pw.Root, "nodes", oldNode.ParentID, oldNode.Name), - filepath.Join(t.pw.Root, "nodes", newNode.ParentID, newNode.Name), + filepath.Join(t.lu.toInternalPath(oldNode.ParentID), oldNode.Name), + filepath.Join(t.lu.toInternalPath(newNode.ParentID), newNode.Name), ) if err != nil { return errors.Wrap(err, "ocisfs: could not move child") } // update parentid and name - tgtPath := filepath.Join(t.pw.Root, "nodes", newNode.ID) + tgtPath := t.lu.toInternalPath(newNode.ID) if err := xattr.Set(tgtPath, parentidAttr, []byte(newNode.ParentID)); err != nil { return errors.Wrap(err, "ocisfs: could not set parentid attribute") @@ -191,7 +191,7 @@ func (t *Tree) Move(ctx context.Context, oldNode *Node, newNode *Node) (err erro // ListFolder lists the content of a folder node func (t *Tree) ListFolder(ctx context.Context, node *Node) ([]*Node, error) { - dir := filepath.Join(t.pw.Root, "nodes", node.ID) + dir := t.lu.toInternalPath(node.ID) f, err := os.Open(dir) if err != nil { if os.IsNotExist(err) { @@ -212,7 +212,7 @@ func (t *Tree) ListFolder(ctx context.Context, node *Node) ([]*Node, error) { continue } n := &Node{ - pw: t.pw, + lu: t.lu, ParentID: node.ID, ID: filepath.Base(link), Name: names[i], @@ -225,12 +225,12 @@ func (t *Tree) ListFolder(ctx context.Context, node *Node) ([]*Node, error) { } // Delete deletes a node in the tree -func (t *Tree) Delete(ctx context.Context, node *Node) (err error) { +func (t *Tree) Delete(ctx context.Context, n *Node) (err error) { // Prepare the trash // TODO use layout?, but it requires resolving the owners user if the username is used instead of the id. // the node knows the owner id so we use that for now - ownerid, _, err := node.Owner() + ownerid, _, err := n.Owner() if err != nil { return } @@ -238,67 +238,80 @@ func (t *Tree) Delete(ctx context.Context, node *Node) (err error) { // fall back to root trash ownerid = "root" } - err = os.MkdirAll(filepath.Join(t.pw.Root, "trash", ownerid), 0700) + err = os.MkdirAll(filepath.Join(t.lu.Options.Root, "trash", ownerid), 0700) if err != nil { return } // get the original path - origin, err := t.pw.Path(ctx, node) + origin, err := t.lu.Path(ctx, n) if err != nil { return } - // remove the entry from the parent dir + // set origin location in metadata + nodePath := t.lu.toInternalPath(n.ID) + if err := xattr.Set(nodePath, trashOriginAttr, []byte(origin)); err != nil { + return err + } - src := filepath.Join(t.pw.Root, "nodes", node.ParentID, node.Name) - err = os.Remove(src) + deletionTime := time.Now().UTC().Format(time.RFC3339Nano) + + // first make node appear in the owners (or root) trash + // parent id and name are stored as extended attributes in the node itself + trashLink := filepath.Join(t.lu.Options.Root, "trash", ownerid, n.ID) + err = os.Symlink("../nodes/"+n.ID+".T."+deletionTime, trashLink) if err != nil { + // To roll back changes + // TODO unset trashOriginAttr return } - // rename the trashed node so it is not picked up when traversing up the tree - nodePath := filepath.Join(t.pw.Root, "nodes", node.ID) - deletionTime := time.Now().UTC().Format(time.RFC3339Nano) + // at this point we have a symlink pointing to a non existing destination, which is fine + + // rename the trashed node so it is not picked up when traversing up the tree and matches the symlink trashPath := nodePath + ".T." + deletionTime err = os.Rename(nodePath, trashPath) if err != nil { + // To roll back changes + // TODO remove symlink + // TODO unset trashOriginAttr return } - // set origin location in metadata - if err := xattr.Set(trashPath, trashOriginAttr, []byte(origin)); err != nil { - return err - } - // make node appear in the owners (or root) trash - // parent id and name are stored as extended attributes in the node itself - trashLink := filepath.Join(t.pw.Root, "trash", ownerid, node.ID) - err = os.Symlink("../nodes/"+node.ID+".T."+deletionTime, trashLink) + // finally remove the entry from the parent dir + src := filepath.Join(t.lu.toInternalPath(n.ParentID), n.Name) + err = os.Remove(src) if err != nil { + // To roll back changes + // TODO revert the rename + // TODO remove symlink + // TODO unset trashOriginAttr return } - p, err := node.Parent() + + p, err := n.Parent() if err != nil { - return + return errors.Wrap(err, "ocisfs: error getting parent "+n.ParentID) } return t.Propagate(ctx, p) } // Propagate propagates changes to the root of the tree func (t *Tree) Propagate(ctx context.Context, n *Node) (err error) { - if !t.pw.TreeTimeAccounting && !t.pw.TreeSizeAccounting { + if !t.lu.Options.TreeTimeAccounting && !t.lu.Options.TreeSizeAccounting { // no propagation enabled log.Debug().Msg("propagation disabled") return } log := appctx.GetLogger(ctx) - nodePath := filepath.Join(t.pw.Root, "nodes", n.ID) + nodePath := t.lu.toInternalPath(n.ID) // is propagation enabled for the parent node? var root *Node - if root, err = t.pw.HomeOrRootNode(ctx); err != nil { + if root, err = t.lu.HomeOrRootNode(ctx); err != nil { return } @@ -323,7 +336,7 @@ func (t *Tree) Propagate(ctx context.Context, n *Node) (err error) { return nil } - if t.pw.TreeTimeAccounting { + if t.lu.Options.TreeTimeAccounting { // update the parent tree time if it is older than the nodes mtime updateSyncTime := false diff --git a/pkg/storage/fs/ocis/upload.go b/pkg/storage/fs/ocis/upload.go index e1c1c5ef91..335aeb2aae 100644 --- a/pkg/storage/fs/ocis/upload.go +++ b/pkg/storage/fs/ocis/upload.go @@ -44,20 +44,46 @@ var defaultFilePerm = os.FileMode(0664) // TODO deprecated ... use tus -func (fs *ocisfs) Upload(ctx context.Context, ref *provider.Reference, r io.ReadCloser) error { +func (fs *ocisfs) Upload(ctx context.Context, ref *provider.Reference, r io.ReadCloser) (err error) { - node, err := fs.pw.NodeFromResource(ctx, ref) - if err != nil { - return err + var n *Node + if n, err = fs.lu.NodeFromResource(ctx, ref); err != nil { + return } - if node.ID == "" { - node.ID = uuid.New().String() + // check permissions + var ok bool + if n.Exists { + // check permissions of file to be overwritten + ok, err = fs.p.HasPermission(ctx, n, func(rp *provider.ResourcePermissions) bool { + return rp.InitiateFileUpload + }) + } else { + // check permissions of parent + p, perr := n.Parent() + if perr != nil { + return errors.Wrap(perr, "ocisfs: error getting parent "+n.ParentID) + } + + ok, err = fs.p.HasPermission(ctx, p, func(rp *provider.ResourcePermissions) bool { + return rp.InitiateFileUpload + }) + } + switch { + case err != nil: + return errtypes.InternalError(err.Error()) + case !ok: + return errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name)) } - nodePath := filepath.Join(fs.pw.Root, "nodes", node.ID) + if n.ID == "" { + n.ID = uuid.New().String() + } - tmp, err := ioutil.TempFile(nodePath, "._reva_atomic_upload") + nodePath := fs.lu.toInternalPath(n.ID) + + var tmp *os.File + tmp, err = ioutil.TempFile(nodePath, "._reva_atomic_upload") if err != nil { return errors.Wrap(err, "ocisfs: error creating tmp fn at "+nodePath) } @@ -69,37 +95,36 @@ func (fs *ocisfs) Upload(ctx context.Context, ref *provider.Reference, r io.Read // TODO move old content to version //_ = os.RemoveAll(path.Join(nodePath, "content")) + appctx.GetLogger(ctx).Warn().Msg("TODO move old content to version") - err = os.Rename(tmp.Name(), nodePath) - if err != nil { - return err + if err = os.Rename(tmp.Name(), nodePath); err != nil { + return } - if fs.pw.EnableHome { + if fs.o.EnableHome { if u, ok := user.ContextGetUser(ctx); ok { - err = node.writeMetadata(u.Id) + err = n.writeMetadata(u.Id) } else { log := appctx.GetLogger(ctx) log.Error().Msg("home support enabled but no user in context") err = errors.Wrap(errtypes.UserRequired("userrequired"), "error getting user from ctx") } } else { - err = node.writeMetadata(nil) + err = n.writeMetadata(nil) } if err != nil { - return err + return } - if fs.pw.TreeTimeAccounting { + if fs.o.TreeTimeAccounting { // mark the home node as the end of propagation q if err = xattr.Set(nodePath, propagationAttr, []byte("1")); err != nil { - appctx.GetLogger(ctx).Error().Err(err).Interface("node", node).Msg("could not mark node to propagate") - return err + appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Msg("could not mark node to propagate") + return } } - return fs.tp.Propagate(ctx, node) - + return fs.tp.Propagate(ctx, n) } // InitiateUpload returns an upload id that can be used for uploads with tus @@ -110,12 +135,14 @@ func (fs *ocisfs) InitiateUpload(ctx context.Context, ref *provider.Reference, u var relative string // the internal path of the file node - node, err := fs.pw.NodeFromResource(ctx, ref) + n, err := fs.lu.NodeFromResource(ctx, ref) if err != nil { return "", err } - relative, err = fs.pw.Path(ctx, node) + // permissions are checked in NewUpload below + + relative, err = fs.lu.Path(ctx, n) if err != nil { return "", err } @@ -132,7 +159,7 @@ func (fs *ocisfs) InitiateUpload(ctx context.Context, ref *provider.Reference, u info.MetaData["mtime"] = metadata["mtime"] } - log.Debug().Interface("info", info).Interface("node", node).Interface("metadata", metadata).Msg("ocisfs: resolved filename") + log.Debug().Interface("info", info).Interface("node", n).Interface("metadata", metadata).Msg("ocisfs: resolved filename") upload, err := fs.NewUpload(ctx, info) if err != nil { @@ -173,12 +200,37 @@ func (fs *ocisfs) NewUpload(ctx context.Context, info tusd.FileInfo) (upload tus } info.MetaData["dir"] = filepath.Clean(info.MetaData["dir"]) - node, err := fs.pw.NodeFromPath(ctx, filepath.Join(info.MetaData["dir"], info.MetaData["filename"])) + n, err := fs.lu.NodeFromPath(ctx, filepath.Join(info.MetaData["dir"], info.MetaData["filename"])) if err != nil { return nil, errors.Wrap(err, "ocisfs: error wrapping filename") } - log.Debug().Interface("info", info).Interface("node", node).Msg("ocisfs: resolved filename") + log.Debug().Interface("info", info).Interface("node", n).Msg("ocisfs: resolved filename") + + // check permissions + var ok bool + if n.Exists { + // check permissions of file to be overwritten + ok, err = fs.p.HasPermission(ctx, n, func(rp *provider.ResourcePermissions) bool { + return rp.InitiateFileUpload + }) + } else { + // check permissions of parent + p, perr := n.Parent() + if perr != nil { + return nil, errors.Wrap(perr, "ocisfs: error getting parent "+n.ParentID) + } + + ok, err = fs.p.HasPermission(ctx, p, func(rp *provider.ResourcePermissions) bool { + return rp.InitiateFileUpload + }) + } + switch { + case err != nil: + return nil, errtypes.InternalError(err.Error()) + case !ok: + return nil, errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name)) + } info.ID = uuid.New().String() @@ -191,9 +243,9 @@ func (fs *ocisfs) NewUpload(ctx context.Context, info tusd.FileInfo) (upload tus "Type": "OCISStore", "BinPath": binPath, - "NodeId": node.ID, - "NodeParentId": node.ParentID, - "NodeName": node.Name, + "NodeId": n.ID, + "NodeParentId": n.ParentID, + "NodeName": n.Name, "Idp": usr.Id.Idp, "UserId": usr.Id.OpaqueId, @@ -212,7 +264,7 @@ func (fs *ocisfs) NewUpload(ctx context.Context, info tusd.FileInfo) (upload tus u := &fileUpload{ info: info, binPath: binPath, - infoPath: filepath.Join(fs.pw.Root, "uploads", info.ID+".info"), + infoPath: filepath.Join(fs.o.Root, "uploads", info.ID+".info"), fs: fs, ctx: ctx, } @@ -237,12 +289,12 @@ func (fs *ocisfs) NewUpload(ctx context.Context, info tusd.FileInfo) (upload tus } func (fs *ocisfs) getUploadPath(ctx context.Context, uploadID string) (string, error) { - return filepath.Join(fs.pw.Root, "uploads", uploadID), nil + return filepath.Join(fs.o.Root, "uploads", uploadID), nil } // GetUpload returns the Upload for the given upload id func (fs *ocisfs) GetUpload(ctx context.Context, id string) (tusd.Upload, error) { - infoPath := filepath.Join(fs.pw.Root, "uploads", id+".info") + infoPath := filepath.Join(fs.o.Root, "uploads", id+".info") info := tusd.FileInfo{} data, err := ioutil.ReadFile(infoPath) @@ -352,7 +404,7 @@ func (upload *fileUpload) writeInfo() error { func (upload *fileUpload) FinishUpload(ctx context.Context) (err error) { n := &Node{ - pw: upload.fs.pw, + lu: upload.fs.lu, ID: upload.info.Storage["NodeId"], ParentID: upload.info.Storage["NodeParentId"], Name: upload.info.Storage["NodeName"], @@ -361,13 +413,13 @@ func (upload *fileUpload) FinishUpload(ctx context.Context) (err error) { if n.ID == "" { n.ID = uuid.New().String() } - targetPath := filepath.Join(upload.fs.pw.Root, "nodes", n.ID) + targetPath := upload.fs.lu.toInternalPath(n.ID) // if target exists create new version var fi os.FileInfo if fi, err = os.Stat(targetPath); err == nil { // versions are stored alongside the actual file, so a rename can be efficient and does not cross storage / partition boundaries - versionsPath := filepath.Join(upload.fs.pw.Root, "nodes", n.ID+".REV."+fi.ModTime().UTC().Format(time.RFC3339Nano)) + versionsPath := upload.fs.lu.toInternalPath(n.ID + ".REV." + fi.ModTime().UTC().Format(time.RFC3339Nano)) if err = os.Rename(targetPath, versionsPath); err != nil { log := appctx.GetLogger(upload.ctx) @@ -390,7 +442,7 @@ func (upload *fileUpload) FinishUpload(ctx context.Context) (err error) { Msg("ocisfs: could not rename") return } - if n.pw.EnableHome { + if upload.fs.o.EnableHome { if u, ok := user.ContextGetUser(upload.ctx); ok { err = n.writeMetadata(u.Id) } else { @@ -404,7 +456,7 @@ func (upload *fileUpload) FinishUpload(ctx context.Context) (err error) { if err != nil { return } - if n.pw.TreeTimeAccounting { + if upload.fs.o.TreeTimeAccounting { // mark the home node as the end of propagation q if err = xattr.Set(targetPath, propagationAttr, []byte("1")); err != nil { appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Msg("could not mark node to propagate") @@ -413,7 +465,7 @@ func (upload *fileUpload) FinishUpload(ctx context.Context) (err error) { } // link child name to parent if it is new - childNameLink := filepath.Join(upload.fs.pw.Root, "nodes", n.ParentID, n.Name) + childNameLink := filepath.Join(upload.fs.lu.toInternalPath(n.ParentID), n.Name) var link string link, err = os.Readlink(childNameLink) if err == nil && link != "../"+n.ID { diff --git a/pkg/storage/fs/owncloud/owncloud.go b/pkg/storage/fs/owncloud/owncloud.go index 2b09a0a636..ab32fe1f42 100644 --- a/pkg/storage/fs/owncloud/owncloud.go +++ b/pkg/storage/fs/owncloud/owncloud.go @@ -446,8 +446,7 @@ func (fs *ocfs) toStorageShadowPath(ctx context.Context, ip string) (sp string) sp = filepath.Join("/", segments[1], segments[3]) } } - log := appctx.GetLogger(ctx) - log.Debug().Str("driver", "ocfs").Str("ipath", ip).Str("spath", sp).Msg("toStorageShadowPath") + appctx.GetLogger(ctx).Debug().Str("driver", "ocfs").Str("ipath", ip).Str("spath", sp).Msg("toStorageShadowPath") return } diff --git a/pkg/storage/fs/owncloud/upload.go b/pkg/storage/fs/owncloud/upload.go index 41909dc309..99c8c2958d 100644 --- a/pkg/storage/fs/owncloud/upload.go +++ b/pkg/storage/fs/owncloud/upload.go @@ -109,27 +109,7 @@ func (fs *ocfs) InitiateUpload(ctx context.Context, ref *provider.Reference, upl return "", errors.Wrap(err, "ocfs: error resolving reference") } - // check permissions - var perm *provider.ResourcePermissions - var perr error - // if destination exists - if _, err := os.Stat(ip); err == nil { - // check permissions of file to be overwritten - perm, perr = fs.readPermissions(ctx, ip) - } else { - // check permissions of parent - perm, perr = fs.readPermissions(ctx, filepath.Dir(ip)) - } - if perr == nil { - if !perm.InitiateFileUpload { - return "", errtypes.PermissionDenied("") - } - } else { - if isNotFound(perr) { - return "", errtypes.NotFound(fs.toStoragePath(ctx, filepath.Dir(ip))) - } - return "", errors.Wrap(perr, "ocfs: error reading permissions") - } + // permissions are checked in NewUpload below p := fs.toStoragePath(ctx, ip) diff --git a/tests/acceptance/expected-failures-on-OC-storage.txt b/tests/acceptance/expected-failures-on-OC-storage.txt index 0dd437a946..166049b341 100644 --- a/tests/acceptance/expected-failures-on-OC-storage.txt +++ b/tests/acceptance/expected-failures-on-OC-storage.txt @@ -96,6 +96,9 @@ apiAuthWebDav/webDavPROPFINDAuth.feature:37 # https://github.com/owncloud/ocis-reva/issues/9 users can access each-others data using the new webdav API apiAuthWebDav/webDavPROPPATCHAuth.feature:38 # +# https://github.com/owncloud/ocis-reva/issues/9 users can access each-others data using the new webdav API +apiAuthWebDav/webDavPUTAuth.feature:38 +# # https://github.com/owncloud/ocis-reva/issues/175 Default capabilities for normal user not same as in oC-core # https://github.com/owncloud/ocis-reva/issues/176 Difference in response content of status.php and default capabilities apiCapabilities/capabilitiesWithNormalUser.feature:11