diff --git a/pkg/storage/fs/ocis/grants.go b/pkg/storage/fs/ocis/grants.go index 5681f72a6d1..b151ef99db4 100644 --- a/pkg/storage/fs/ocis/grants.go +++ b/pkg/storage/fs/ocis/grants.go @@ -2,23 +2,20 @@ package ocis import ( "context" - "encoding/csv" - "fmt" "path/filepath" - "strconv" "strings" - userpb "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/errtypes" + "github.com/cs3org/reva/pkg/storage/utils/ace" "github.com/pkg/xattr" ) func (fs *ocisfs) AddGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) (err error) { log := appctx.GetLogger(ctx) log.Debug().Interface("ref", ref).Interface("grant", g).Msg("AddGrant()") - var node *NodeInfo + var node *Node if node, err = fs.pw.NodeFromResource(ctx, ref); err != nil { return } @@ -27,214 +24,17 @@ func (fs *ocisfs) AddGrant(ctx context.Context, ref *provider.Reference, g *prov return } - e, err := fs.getACE(g) - if err != nil { - return err - } - - var attr string - if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP { - attr = sharePrefix + "g:" + e.Principal - } else { - attr = sharePrefix + "u:" + e.Principal - } - np := filepath.Join(fs.pw.Root(), "nodes", node.ID) - if err := xattr.Set(np, attr, getValue(e)); err != nil { + e := ace.FromGrant(g) + principal, value := e.Marshal() + if err := xattr.Set(np, sharePrefix+principal, value); err != nil { return err } return fs.tp.Propagate(ctx, node) } -func getValue(e *ace) []byte { - // first byte will be replaced after converting to byte array - val := fmt.Sprintf("_t=%s:f=%s:p=%s", e.Type, e.Flags, e.Permissions) - b := []byte(val) - b[0] = 0 // indicate key value - return b -} - -func getACEPerm(set *provider.ResourcePermissions) (string, error) { - var b strings.Builder - - if set.Stat || set.InitiateFileDownload || set.ListContainer { - b.WriteString("r") - } - if set.InitiateFileUpload || set.Move { - b.WriteString("w") - } - if set.CreateContainer { - b.WriteString("a") - } - if set.Delete { - b.WriteString("d") - } - - // sharing - if set.AddGrant || set.RemoveGrant || set.UpdateGrant { - b.WriteString("C") - } - if set.ListGrants { - b.WriteString("c") - } - - // trash - if set.ListRecycle { - b.WriteString("u") - } - if set.RestoreRecycleItem { - b.WriteString("U") - } - if set.PurgeRecycle { - b.WriteString("P") - } - - // versions - if set.ListFileVersions { - b.WriteString("v") - } - if set.RestoreFileVersion { - b.WriteString("V") - } - - // quota - if set.GetQuota { - b.WriteString("q") - } - // TODO set quota permission? - // TODO GetPath - return b.String(), nil -} - -func (fs *ocisfs) getACE(g *provider.Grant) (*ace, error) { - permissions, err := getACEPerm(g.Permissions) - if err != nil { - return nil, err - } - e := &ace{ - Principal: g.Grantee.Id.OpaqueId, - Permissions: permissions, - // TODO creator ... - Type: "A", - } - if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP { - e.Flags = "g" - } - return e, nil -} - -type ace struct { - //NFSv4 acls - Type string // t - Flags string // f - Principal string // im key - Permissions string // p - - // sharing specific - ShareTime int // s - Creator string // c - Expires int // e - Password string // w passWord TODO h = hash - Label string // l -} - -func unmarshalACE(v []byte) (*ace, error) { - // first byte indicates type of value - switch v[0] { - case 0: // = ':' separated key=value pairs - s := string(v[1:]) - return unmarshalKV(s) - default: - return nil, fmt.Errorf("unknown ace encoding") - } -} - -func unmarshalKV(s string) (*ace, error) { - e := &ace{} - r := csv.NewReader(strings.NewReader(s)) - r.Comma = ':' - r.Comment = 0 - r.FieldsPerRecord = -1 - r.LazyQuotes = false - r.TrimLeadingSpace = false - records, err := r.ReadAll() - if err != nil { - return nil, err - } - if len(records) != 1 { - return nil, fmt.Errorf("more than one row of ace kvs") - } - for i := range records[0] { - kv := strings.Split(records[0][i], "=") - switch kv[0] { - case "t": - e.Type = kv[1] - case "f": - e.Flags = kv[1] - case "p": - e.Permissions = kv[1] - case "s": - v, err := strconv.Atoi(kv[1]) - if err != nil { - return nil, err - } - e.ShareTime = v - case "c": - e.Creator = kv[1] - case "e": - v, err := strconv.Atoi(kv[1]) - if err != nil { - return nil, err - } - e.Expires = v - case "w": - e.Password = kv[1] - case "l": - e.Label = kv[1] - // TODO default ... log unknown keys? or add as opaque? hm we need that for tagged shares ... - } - } - return e, nil -} - -// Parse parses an acl string with the given delimiter (LongTextForm or ShortTextForm) -func getACEs(ctx context.Context, fsfn string, attrs []string) (entries []*ace, err error) { - log := appctx.GetLogger(ctx) - entries = []*ace{} - for i := range attrs { - if strings.HasPrefix(attrs[i], sharePrefix) { - principal := attrs[i][len(sharePrefix):] - var value []byte - if value, err = xattr.Get(fsfn, attrs[i]); err != nil { - log.Error().Err(err).Str("attr", attrs[i]).Msg("could not read attribute") - continue - } - var e *ace - if e, err = unmarshalACE(value); err != nil { - log.Error().Err(err).Str("attr", attrs[i]).Msg("could unmarshal ace") - continue - } - e.Principal = principal[2:] - // check consistency of Flags and principal type - if strings.Contains(e.Flags, "g") { - if principal[:1] != "g" { - log.Error().Str("attr", attrs[i]).Interface("ace", e).Msg("inconsistent ace: expected group") - continue - } - } else { - if principal[:1] != "u" { - log.Error().Str("attr", attrs[i]).Interface("ace", e).Msg("inconsistent ace: expected user") - continue - } - } - entries = append(entries, e) - } - } - return entries, nil -} - func (fs *ocisfs) ListGrants(ctx context.Context, ref *provider.Reference) (grants []*provider.Grant, err error) { - var node *NodeInfo + var node *Node if node, err = fs.pw.NodeFromResource(ctx, ref); err != nil { return } @@ -251,107 +51,19 @@ func (fs *ocisfs) ListGrants(ctx context.Context, ref *provider.Reference) (gran } log.Debug().Interface("attrs", attrs).Msg("read attributes") - // filter attributes - var aces []*ace - if aces, err = getACEs(ctx, np, attrs); err != nil { - log.Error().Err(err).Msg("error getting aces") - return nil, err - } + + aces := extractACEsFromAttrs(ctx, np, attrs) grants = make([]*provider.Grant, 0, len(aces)) for i := range aces { - grantee := &provider.Grantee{ - // TODO lookup uid from principal - Id: &userpb.UserId{OpaqueId: aces[i].Principal}, - Type: fs.getGranteeType(aces[i]), - } - grants = append(grants, &provider.Grant{ - Grantee: grantee, - Permissions: fs.getGrantPermissionSet(aces[i].Permissions), - }) + grants = append(grants, aces[i].Grant()) } return grants, nil } -func (fs *ocisfs) getGranteeType(e *ace) provider.GranteeType { - if strings.Contains(e.Flags, "g") { - return provider.GranteeType_GRANTEE_TYPE_GROUP - } - return provider.GranteeType_GRANTEE_TYPE_USER -} - -func (fs *ocisfs) getGrantPermissionSet(mode string) *provider.ResourcePermissions { - p := &provider.ResourcePermissions{} - // r - if strings.Contains(mode, "r") { - p.Stat = true - p.InitiateFileDownload = true - p.ListContainer = true - } - // w - if strings.Contains(mode, "w") { - p.InitiateFileUpload = true - if p.InitiateFileDownload { - p.Move = true - } - } - //a - if strings.Contains(mode, "a") { - // TODO append data to file permission? - p.CreateContainer = true - } - //x - //if strings.Contains(mode, "x") { - // TODO execute file permission? - // TODO change directory permission? - //} - //d - if strings.Contains(mode, "d") { - p.Delete = true - } - //D ? - - // sharing - if strings.Contains(mode, "C") { - p.AddGrant = true - p.RemoveGrant = true - p.UpdateGrant = true - } - if strings.Contains(mode, "c") { - p.ListGrants = true - } - - // trash - if strings.Contains(mode, "u") { // u = undelete - p.ListRecycle = true - } - if strings.Contains(mode, "U") { - p.RestoreRecycleItem = true - } - if strings.Contains(mode, "P") { - p.PurgeRecycle = true - } - - // versions - if strings.Contains(mode, "v") { - p.ListFileVersions = true - } - if strings.Contains(mode, "V") { - p.RestoreFileVersion = true - } - - // ? - // TODO GetPath - if strings.Contains(mode, "q") { - p.GetQuota = true - } - // TODO set quota permission? - return p -} - func (fs *ocisfs) RemoveGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) (err error) { - var node *NodeInfo + var node *Node if node, err = fs.pw.NodeFromResource(ctx, ref); err != nil { return } @@ -378,3 +90,27 @@ 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 { return fs.AddGrant(ctx, ref, g) } + +// extractACEsFromAttrs reads ACEs in the list of attrs from the node +func extractACEsFromAttrs(ctx context.Context, fsfn string, attrs []string) (entries []*ace.ACE) { + log := appctx.GetLogger(ctx) + entries = []*ace.ACE{} + for i := range attrs { + if strings.HasPrefix(attrs[i], sharePrefix) { + var value []byte + var err error + if value, err = xattr.Get(fsfn, attrs[i]); err != nil { + log.Error().Err(err).Str("attr", attrs[i]).Msg("could not read attribute") + continue + } + var e *ace.ACE + principal := attrs[i][len(sharePrefix):] + 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 + } + entries = append(entries, e) + } + } + return +} diff --git a/pkg/storage/fs/ocis/node.go b/pkg/storage/fs/ocis/node.go index 295c3b5d446..16ef1d00a3a 100644 --- a/pkg/storage/fs/ocis/node.go +++ b/pkg/storage/fs/ocis/node.go @@ -1,17 +1,23 @@ package ocis import ( + "context" "fmt" "os" "path/filepath" "strings" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + 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/mime" "github.com/pkg/errors" "github.com/pkg/xattr" ) -// NodeInfo allows referencing a node by id and optionally a relative path -type NodeInfo struct { +// Node represents a node in the tree and provides methods to get a Parent or Child instance +type Node struct { pw PathWrapper ParentID string ID string @@ -22,8 +28,8 @@ type NodeInfo struct { } // NewNode creates a new instance and checks if it exists -func NewNode(pw PathWrapper, id string) (n *NodeInfo, err error) { - n = &NodeInfo{ +func NewNode(pw PathWrapper, id string) (n *Node, err error) { + n = &Node{ pw: pw, ID: id, } @@ -35,14 +41,12 @@ func NewNode(pw PathWrapper, id string) (n *NodeInfo, err error) { if attrBytes, err = xattr.Get(nodePath, "user.ocis.parentid"); err == nil { n.ParentID = string(attrBytes) } else { - // TODO log error return } // lookup name in extended attributes if attrBytes, err = xattr.Get(nodePath, "user.ocis.name"); err == nil { n.Name = string(attrBytes) } else { - // TODO log error return } @@ -57,10 +61,8 @@ func NewNode(pw PathWrapper, id string) (n *NodeInfo, err error) { } else { if os.IsNotExist(err) { return - } else { - // TODO log error - return } + return } } @@ -70,8 +72,8 @@ func NewNode(pw PathWrapper, id string) (n *NodeInfo, err error) { } // Child returns the child node with the given name -func (n *NodeInfo) Child(name string) (c *NodeInfo, err error) { - c = &NodeInfo{ +func (n *Node) Child(name string) (c *Node, err error) { + c = &Node{ pw: n.pw, ParentID: n.ID, Name: name, @@ -95,16 +97,16 @@ func (n *NodeInfo) Child(name string) (c *NodeInfo, err error) { } // IsRoot returns true when the node is the root of a tree -func (n *NodeInfo) IsRoot() bool { +func (n *Node) IsRoot() bool { return n.ParentID == "root" } // Parent returns the parent node -func (n *NodeInfo) Parent() (p *NodeInfo, err error) { +func (n *Node) Parent() (p *Node, err error) { if n.ParentID == "root" { return nil, fmt.Errorf("ocisfs: root has no parent") } - p = &NodeInfo{ + p = &Node{ pw: n.pw, ID: n.ParentID, } @@ -133,7 +135,7 @@ func (n *NodeInfo) Parent() (p *NodeInfo, err error) { } // Owner returns the cached owner id or reads it from the extended attributes -func (n *NodeInfo) Owner() (id string, idp string, err error) { +func (n *Node) Owner() (id string, idp string, err error) { if n.ownerID != "" && n.ownerIDP != "" { return n.ownerID, n.ownerIDP, nil } @@ -145,13 +147,77 @@ func (n *NodeInfo) Owner() (id string, idp string, err error) { if attrBytes, err = xattr.Get(nodePath, "user.ocis.owner.id"); err == nil { n.ownerID = string(attrBytes) } else { - // TODO log error + return } // lookup name in extended attributes if attrBytes, err = xattr.Get(nodePath, "user.ocis.owner.idp"); err == nil { n.ownerIDP = string(attrBytes) } else { - // TODO log error + return } return n.ownerID, n.ownerIDP, err } + +// AsResourceInfo return the node as CS3 ResourceInfo +func (n *Node) AsResourceInfo(ctx context.Context) (ri *provider.ResourceInfo, err error) { + var fn string + + nodePath := filepath.Join(n.pw.Root(), "nodes", n.ID) + + var fi os.FileInfo + + nodeType := provider.ResourceType_RESOURCE_TYPE_INVALID + if fi, err = os.Lstat(nodePath); err != nil { + return + } + if fi.IsDir() { + nodeType = provider.ResourceType_RESOURCE_TYPE_CONTAINER + } else if fi.Mode().IsRegular() { + nodeType = provider.ResourceType_RESOURCE_TYPE_FILE + } else if fi.Mode()&os.ModeSymlink != 0 { + nodeType = provider.ResourceType_RESOURCE_TYPE_SYMLINK + // TODO reference using ext attr on a symlink + // nodeType = provider.ResourceType_RESOURCE_TYPE_REFERENCE + } + + var etag []byte + // TODO optionally store etag in new `root/attributes/` file + if etag, err = xattr.Get(nodePath, "user.ocis.etag"); err != nil { + log := appctx.GetLogger(ctx) + log.Error().Err(err).Interface("node", n).Msg("could not read etag") + } + + id := &provider.ResourceId{OpaqueId: n.ID} + + fn, err = n.pw.Path(ctx, n) + if err != nil { + return nil, err + } + ri = &provider.ResourceInfo{ + Id: id, + Path: fn, + Type: nodeType, + Etag: string(etag), + MimeType: mime.Detect(nodeType == provider.ResourceType_RESOURCE_TYPE_CONTAINER, fn), + Size: uint64(fi.Size()), + // TODO fix permissions + PermissionSet: &provider.ResourcePermissions{ListContainer: true, CreateContainer: true}, + Mtime: &types.Timestamp{ + Seconds: uint64(fi.ModTime().Unix()), + // TODO read nanos from where? Nanos: fi.MTimeNanos, + }, + } + + if owner, idp, err := n.Owner(); err == nil { + ri.Owner = &userpb.UserId{ + Idp: idp, + OpaqueId: owner, + } + } + log := appctx.GetLogger(ctx) + log.Debug(). + Interface("ri", ri). + Msg("AsResourceInfo") + + return ri, nil +} diff --git a/pkg/storage/fs/ocis/ocis.go b/pkg/storage/fs/ocis/ocis.go index 8a52b5c3600..caf5e981e65 100644 --- a/pkg/storage/fs/ocis/ocis.go +++ b/pkg/storage/fs/ocis/ocis.go @@ -25,13 +25,9 @@ import ( "os" "path/filepath" - userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" 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/logger" - "github.com/cs3org/reva/pkg/mime" "github.com/cs3org/reva/pkg/storage" "github.com/cs3org/reva/pkg/storage/fs/registry" "github.com/cs3org/reva/pkg/storage/utils/templates" @@ -294,7 +290,7 @@ func (fs *ocisfs) GetPathByID(ctx context.Context, id *provider.ResourceId) (str } func (fs *ocisfs) CreateDir(ctx context.Context, fn string) (err error) { - var node *NodeInfo + var node *Node if node, err = fs.pw.NodeFromPath(ctx, fn); err != nil { return } @@ -306,7 +302,7 @@ func (fs *ocisfs) CreateReference(ctx context.Context, path string, targetURI *u } func (fs *ocisfs) Move(ctx context.Context, oldRef, newRef *provider.Reference) (err error) { - var oldNode, newNode *NodeInfo + var oldNode, newNode *Node if oldNode, err = fs.pw.NodeFromResource(ctx, oldRef); err != nil { return } @@ -322,7 +318,7 @@ func (fs *ocisfs) Move(ctx context.Context, oldRef, newRef *provider.Reference) } func (fs *ocisfs) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []string) (ri *provider.ResourceInfo, err error) { - var node *NodeInfo + var node *Node if node, err = fs.pw.NodeFromResource(ctx, ref); err != nil { return } @@ -330,11 +326,11 @@ func (fs *ocisfs) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []s err = errtypes.NotFound(filepath.Join(node.ParentID, node.Name)) return } - return fs.normalize(ctx, node) + return node.AsResourceInfo(ctx) } func (fs *ocisfs) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys []string) (finfos []*provider.ResourceInfo, err error) { - var node *NodeInfo + var node *Node if node, err = fs.pw.NodeFromResource(ctx, ref); err != nil { return } @@ -342,14 +338,14 @@ func (fs *ocisfs) ListFolder(ctx context.Context, ref *provider.Reference, mdKey err = errtypes.NotFound(filepath.Join(node.ParentID, node.Name)) return } - var children []*NodeInfo + var children []*Node children, err = fs.tp.ListFolder(ctx, node) if err != nil { return } for i := range children { - if ri, err := fs.normalize(ctx, children[i]); err == nil { + if ri, err := children[i].AsResourceInfo(ctx); err == nil { finfos = append(finfos, ri) } } @@ -357,7 +353,7 @@ 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 *NodeInfo + var node *Node if node, err = fs.pw.NodeFromResource(ctx, ref); err != nil { return } @@ -370,7 +366,7 @@ func (fs *ocisfs) Delete(ctx context.Context, ref *provider.Reference) (err erro // Data persistence -func (fs *ocisfs) ContentPath(node *NodeInfo) string { +func (fs *ocisfs) ContentPath(node *Node) string { return filepath.Join(fs.conf.Root, "nodes", node.ID) } @@ -404,68 +400,3 @@ func (fs *ocisfs) Download(ctx context.Context, ref *provider.Reference) (io.Rea // Trash persistence in recycle.go // share persistence in grants.go - -// supporting functions - -func (fs *ocisfs) normalize(ctx context.Context, node *NodeInfo) (ri *provider.ResourceInfo, err error) { - var fn string - - nodePath := filepath.Join(fs.conf.Root, "nodes", node.ID) - - var fi os.FileInfo - - nodeType := provider.ResourceType_RESOURCE_TYPE_INVALID - if fi, err = os.Lstat(nodePath); err != nil { - return - } - if fi.IsDir() { - nodeType = provider.ResourceType_RESOURCE_TYPE_CONTAINER - } else if fi.Mode().IsRegular() { - nodeType = provider.ResourceType_RESOURCE_TYPE_FILE - } else if fi.Mode()&os.ModeSymlink != 0 { - nodeType = provider.ResourceType_RESOURCE_TYPE_SYMLINK - // TODO reference using ext attr on a symlink - // nodeType = provider.ResourceType_RESOURCE_TYPE_REFERENCE - } - - var etag []byte - // TODO optionally store etag in new `root/attributes/` file - if etag, err = xattr.Get(nodePath, "user.ocis.etag"); err != nil { - log := appctx.GetLogger(ctx) - log.Debug().Err(err).Msg("could not read etag") - } - - id := &provider.ResourceId{OpaqueId: node.ID} - // Path changes the node because it traverses the tree - fn, err = fs.pw.Path(ctx, node) - if err != nil { - return nil, err - } - ri = &provider.ResourceInfo{ - Id: id, - Path: fn, - Type: nodeType, - Etag: string(etag), - MimeType: mime.Detect(nodeType == provider.ResourceType_RESOURCE_TYPE_CONTAINER, fn), - Size: uint64(fi.Size()), - // TODO fix permissions - PermissionSet: &provider.ResourcePermissions{ListContainer: true, CreateContainer: true}, - Mtime: &types.Timestamp{ - Seconds: uint64(fi.ModTime().Unix()), - // TODO read nanos from where? Nanos: fi.MTimeNanos, - }, - } - - if owner, idp, err := node.Owner(); err == nil { - ri.Owner = &userpb.UserId{ - Idp: idp, - OpaqueId: owner, - } - } - log := appctx.GetLogger(ctx) - log.Debug(). - Interface("ri", ri). - Msg("normalized") - - return ri, nil -} diff --git a/pkg/storage/fs/ocis/path.go b/pkg/storage/fs/ocis/path.go index c23b0ba09de..98dbc3a4c86 100644 --- a/pkg/storage/fs/ocis/path.go +++ b/pkg/storage/fs/ocis/path.go @@ -19,18 +19,17 @@ type Path struct { // ocis fs works on top of a dir of uuid nodes root string `mapstructure:"root"` - // UserLayout wraps the internal path with user information. - // Example: if conf.Namespace is /ocis/user and received path is /docs - // and the UserLayout is {{.Username}} the internal path will be: - // /ocis/user//docs + // UserLayout wraps the internal path in the users folder with user information. UserLayout string `mapstructure:"user_layout"` + // TODO NodeLayout option to save nodes as eg. nodes/1d/d8/1dd84abf-9466-4e14-bb86-02fc4ea3abcf + // EnableHome enables the creation of home directories. EnableHome bool `mapstructure:"enable_home"` } -// NodeFromResource takes in a request path or request id and converts it to a NodeInfo -func (pw *Path) NodeFromResource(ctx context.Context, ref *provider.Reference) (*NodeInfo, error) { +// 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) { if ref.GetPath() != "" { return pw.NodeFromPath(ctx, ref.GetPath()) } @@ -43,8 +42,8 @@ func (pw *Path) NodeFromResource(ctx context.Context, ref *provider.Reference) ( return nil, fmt.Errorf("invalid reference %+v", ref) } -// NodeFromPath converts a filename into a NodeInfo -func (pw *Path) NodeFromPath(ctx context.Context, fn string) (node *NodeInfo, err error) { +// NodeFromPath converts a filename into a Node +func (pw *Path) NodeFromPath(ctx context.Context, fn string) (node *Node, err error) { log := appctx.GetLogger(ctx) log.Debug().Interface("fn", fn).Msg("NodeFromPath()") node, err = pw.RootNode(ctx) @@ -69,7 +68,7 @@ func (pw *Path) NodeFromPath(ctx context.Context, fn string) (node *NodeInfo, er } // NodeFromID returns the internal path for the id -func (pw *Path) NodeFromID(ctx context.Context, id *provider.ResourceId) (n *NodeInfo, err error) { +func (pw *Path) 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) } @@ -77,20 +76,15 @@ func (pw *Path) NodeFromID(ctx context.Context, id *provider.ResourceId) (n *Nod } // Path returns the path for node -func (pw *Path) Path(ctx context.Context, n *NodeInfo) (p string, err error) { - log := appctx.GetLogger(ctx) - log.Debug().Interface("node", n).Msg("Path()") - /* - // check if this a not yet existing node - if n.ID == "" && n.Name != "" && n.ParentID != "" { - path = n.Name - n, err = n.Parent() - } - */ +func (pw *Path) Path(ctx context.Context, n *Node) (p string, err error) { for !n.IsRoot() { p = filepath.Join(n.Name, p) if n, err = n.Parent(); err != nil { - log.Error().Err(err).Str("path", p).Interface("node", n).Msg("Path()") + appctx.GetLogger(ctx). + Error().Err(err). + Str("path", p). + Interface("node", n). + Msg("Path()") return } } @@ -98,7 +92,7 @@ func (pw *Path) Path(ctx context.Context, n *NodeInfo) (p string, err error) { } // readRootLink reads the symbolic link and extracts the node id -func (pw *Path) readRootLink(root string) (node *NodeInfo, err error) { +func (pw *Path) readRootLink(root string) (node *Node, err error) { // A root symlink looks like `../nodes/76455834-769e-412a-8a01-68f265365b79` link, err := os.Readlink(root) if os.IsNotExist(err) { @@ -108,7 +102,7 @@ func (pw *Path) readRootLink(root string) (node *NodeInfo, err error) { // extract the nodeID if strings.HasPrefix(link, "../nodes/") { - node = &NodeInfo{ + node = &Node{ pw: pw, ID: filepath.Base(link), ParentID: "root", @@ -122,7 +116,7 @@ func (pw *Path) readRootLink(root string) (node *NodeInfo, err error) { // RootNode returns the root node of a tree, // taking into account the user layout if EnableHome is true -func (pw *Path) RootNode(ctx context.Context) (node *NodeInfo, err error) { +func (pw *Path) RootNode(ctx context.Context) (node *Node, err error) { var root string if pw.EnableHome && pw.UserLayout != "" { // start at the users root node diff --git a/pkg/storage/fs/ocis/persistence.go b/pkg/storage/fs/ocis/persistence.go index c73f736407d..7b86d14fb4a 100644 --- a/pkg/storage/fs/ocis/persistence.go +++ b/pkg/storage/fs/ocis/persistence.go @@ -11,28 +11,28 @@ import ( // TreePersistence is used to manage a tree hierarchy type TreePersistence interface { GetPathByID(ctx context.Context, id *provider.ResourceId) (string, error) - GetMD(ctx context.Context, node *NodeInfo) (os.FileInfo, error) - ListFolder(ctx context.Context, node *NodeInfo) ([]*NodeInfo, error) - CreateDir(ctx context.Context, node *NodeInfo) (err error) + GetMD(ctx context.Context, node *Node) (os.FileInfo, error) + ListFolder(ctx context.Context, node *Node) ([]*Node, error) + CreateDir(ctx context.Context, node *Node) (err error) CreateReference(ctx context.Context, path string, targetURI *url.URL) error - Move(ctx context.Context, oldNode *NodeInfo, newNode *NodeInfo) (err error) - Delete(ctx context.Context, node *NodeInfo) (err error) + Move(ctx context.Context, oldNode *Node, newNode *Node) (err error) + Delete(ctx context.Context, node *Node) (err error) - Propagate(ctx context.Context, node *NodeInfo) (err error) + Propagate(ctx context.Context, node *Node) (err error) } // PathWrapper is used to encapsulate path transformations type PathWrapper interface { - NodeFromResource(ctx context.Context, ref *provider.Reference) (node *NodeInfo, err error) - NodeFromID(ctx context.Context, id *provider.ResourceId) (node *NodeInfo, err error) + NodeFromResource(ctx context.Context, ref *provider.Reference) (node *Node, err error) + NodeFromID(ctx context.Context, id *provider.ResourceId) (node *Node, err error) - // Wrap returns a NodeInfo object: + // Wrap returns a Node object: // - if the node exists with the node id, name and parent // - if only the parent exists, the node id is empty - NodeFromPath(ctx context.Context, fn string) (node *NodeInfo, err error) - Path(ctx context.Context, node *NodeInfo) (path string, err error) + NodeFromPath(ctx context.Context, fn string) (node *Node, err error) + Path(ctx context.Context, node *Node) (path string, err error) - RootNode(ctx context.Context) (node *NodeInfo, err error) + RootNode(ctx context.Context) (node *Node, err error) // Root returns the internal root of the storage Root() string } diff --git a/pkg/storage/fs/ocis/revisions.go b/pkg/storage/fs/ocis/revisions.go index f93e77be56f..f02d1164950 100644 --- a/pkg/storage/fs/ocis/revisions.go +++ b/pkg/storage/fs/ocis/revisions.go @@ -11,7 +11,7 @@ import ( ) func (fs *ocisfs) ListRevisions(ctx context.Context, ref *provider.Reference) (revisions []*provider.FileVersion, err error) { - var node *NodeInfo + var node *Node if node, err = fs.pw.NodeFromResource(ctx, ref); err != nil { return } diff --git a/pkg/storage/fs/ocis/tree.go b/pkg/storage/fs/ocis/tree.go index e007ad6138d..993422117f4 100644 --- a/pkg/storage/fs/ocis/tree.go +++ b/pkg/storage/fs/ocis/tree.go @@ -33,7 +33,7 @@ func NewTree(pw PathWrapper, dataDirectory string) (TreePersistence, error) { } // GetMD returns the metadata of a node in the tree -func (t *Tree) GetMD(ctx context.Context, node *NodeInfo) (os.FileInfo, error) { +func (t *Tree) GetMD(ctx context.Context, node *Node) (os.FileInfo, error) { md, err := os.Stat(filepath.Join(t.DataDirectory, "nodes", node.ID)) if err != nil { if os.IsNotExist(err) { @@ -47,7 +47,7 @@ func (t *Tree) GetMD(ctx context.Context, node *NodeInfo) (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 *NodeInfo + var node *Node node, err = t.pw.NodeFromID(ctx, id) if err != nil { return @@ -58,7 +58,7 @@ func (t *Tree) GetPathByID(ctx context.Context, id *provider.ResourceId) (relati } // CreateDir creates a new directory entry in the tree -func (t *Tree) CreateDir(ctx context.Context, node *NodeInfo) (err error) { +func (t *Tree) CreateDir(ctx context.Context, node *Node) (err error) { // TODO always try to fill node? if node.Exists || node.ID != "" { // child already exists @@ -102,11 +102,12 @@ func (t *Tree) CreateDir(ctx context.Context, node *NodeInfo) (err error) { // CreateReference creates a new reference entry in the tree func (t *Tree) CreateReference(ctx context.Context, path string, targetURI *url.URL) error { + // TODO use symlink, but set extended attribute on link (not on target) return errtypes.NotSupported("operation not supported: CreateReference") } // Move replaces the target with the source -func (t *Tree) Move(ctx context.Context, oldNode *NodeInfo, newNode *NodeInfo) (err error) { +func (t *Tree) Move(ctx context.Context, oldNode *Node, newNode *Node) (err error) { // if target exists delete it without trashing it if newNode.Exists { // TODO make sure all children are deleted @@ -175,7 +176,7 @@ func (t *Tree) Move(ctx context.Context, oldNode *NodeInfo, newNode *NodeInfo) ( } // ListFolder lists the content of a folder node -func (t *Tree) ListFolder(ctx context.Context, node *NodeInfo) ([]*NodeInfo, error) { +func (t *Tree) ListFolder(ctx context.Context, node *Node) ([]*Node, error) { dir := filepath.Join(t.DataDirectory, "nodes", node.ID) f, err := os.Open(dir) @@ -187,14 +188,14 @@ func (t *Tree) ListFolder(ctx context.Context, node *NodeInfo) ([]*NodeInfo, err } names, err := f.Readdirnames(0) - nodes := []*NodeInfo{} + nodes := []*Node{} for i := range names { link, err := os.Readlink(filepath.Join(dir, names[i])) if err != nil { // TODO log continue } - n := &NodeInfo{ + n := &Node{ pw: t.pw, ParentID: node.ID, ID: filepath.Base(link), @@ -208,7 +209,7 @@ func (t *Tree) ListFolder(ctx context.Context, node *NodeInfo) ([]*NodeInfo, err } // Delete deletes a node in the tree -func (t *Tree) Delete(ctx context.Context, node *NodeInfo) (err error) { +func (t *Tree) Delete(ctx context.Context, node *Node) (err error) { src := filepath.Join(t.DataDirectory, "nodes", node.ParentID, node.Name) err = os.Remove(src) if err != nil { @@ -237,7 +238,7 @@ func (t *Tree) Delete(ctx context.Context, node *NodeInfo) (err error) { } // Propagate propagates changes to the root of the tree -func (t *Tree) Propagate(ctx context.Context, node *NodeInfo) (err error) { +func (t *Tree) Propagate(ctx context.Context, node *Node) (err error) { // generate an etag bytes := make([]byte, 16) if _, err := rand.Read(bytes); err != nil { diff --git a/pkg/storage/fs/ocis/upload.go b/pkg/storage/fs/ocis/upload.go index e9857b7e447..e372b7f4c46 100644 --- a/pkg/storage/fs/ocis/upload.go +++ b/pkg/storage/fs/ocis/upload.go @@ -326,7 +326,7 @@ func (upload *fileUpload) writeInfo() error { // FinishUpload finishes an upload and moves the file to the internal destination func (upload *fileUpload) FinishUpload(ctx context.Context) error { - n := &NodeInfo{ + n := &Node{ pw: upload.fs.pw, ID: upload.info.Storage["NodeId"], ParentID: upload.info.Storage["NodeParentId"], diff --git a/pkg/storage/fs/owncloud/owncloud.go b/pkg/storage/fs/owncloud/owncloud.go index cb51617be4d..c899caf9329 100644 --- a/pkg/storage/fs/owncloud/owncloud.go +++ b/pkg/storage/fs/owncloud/owncloud.go @@ -20,7 +20,6 @@ package owncloud import ( "context" - "encoding/csv" "fmt" "io" "io/ioutil" @@ -44,6 +43,7 @@ import ( "github.com/cs3org/reva/pkg/sharedconf" "github.com/cs3org/reva/pkg/storage" "github.com/cs3org/reva/pkg/storage/fs/registry" + "github.com/cs3org/reva/pkg/storage/utils/ace" "github.com/cs3org/reva/pkg/storage/utils/templates" "github.com/cs3org/reva/pkg/user" "github.com/gofrs/uuid" @@ -64,83 +64,6 @@ const ( // idAttribute is the name of the filesystem extended attribute that is used to store the uuid in idAttribute string = "user.oc.id" - // shares are persisted using extended attributes. We are going to mimic - // NFS4 ACLs, with one extended attribute per share, following Access - // Control Entries (ACEs). The following is taken from the nfs4_acl man page, - // see https://linux.die.net/man/5/nfs4_acl: - // the extended attributes will look like this - // "user.oc.acl.:::" - // - *type* will be limited to A for now - // A: Allow - allow *principal* to perform actions requiring *permissions* - // In the future we can use: - // U: aUdit - log any attempted access by principal which requires - // permissions. - // L: aLarm - generate a system alarm at any attempted access by - // principal which requires permissions - // D for deny is not recommended - // - *flags* for now empty or g for group, no inheritance yet - // - d directory-inherit - newly-created subdirectories will inherit the - // ACE. - // - f file-inherit - newly-created files will inherit the ACE, minus its - // inheritance flags. Newly-created subdirectories - // will inherit the ACE; if directory-inherit is not - // also specified in the parent ACE, inherit-only will - // be added to the inherited ACE. - // - n no-propagate-inherit - newly-created subdirectories will inherit - // the ACE, minus its inheritance flags. - // - i inherit-only - the ACE is not considered in permissions checks, - // but it is heritable; however, the inherit-only - // flag is stripped from inherited ACEs. - // - *principal* a named user, group or special principal - // - the oidc sub@iss maps nicely to this - // - 'OWNER@', 'GROUP@', and 'EVERYONE@', which are, respectively, analogous to the POSIX user/group/other - // - *permissions* - // - r read-data (files) / list-directory (directories) - // - w write-data (files) / create-file (directories) - // - a append-data (files) / create-subdirectory (directories) - // - x execute (files) / change-directory (directories) - // - d delete - delete the file/directory. Some servers will allow a delete to occur if either this permission is set in the file/directory or if the delete-child permission is set in its parent directory. - // - D delete-child - remove a file or subdirectory from within the given directory (directories only) - // - t read-attributes - read the attributes of the file/directory. - // - T write-attributes - write the attributes of the file/directory. - // - n read-named-attributes - read the named attributes of the file/directory. - // - N write-named-attributes - write the named attributes of the file/directory. - // - c read-ACL - read the file/directory NFSv4 ACL. - // - C write-ACL - write the file/directory NFSv4 ACL. - // - o write-owner - change ownership of the file/directory. - // - y synchronize - allow clients to use synchronous I/O with the server. - // TODO implement OWNER@ as "user.oc.acl.A::OWNER@:rwaDxtTnNcCy" - // attribute names are limited to 255 chars by the linux kernel vfs, values to 64 kb - // ext3 extended attributes must fit inside a single filesystem block ... 4096 bytes - // that leaves us with "user.oc.acl.A::someonewithaslightlylongersubject@whateverissuer:rwaDxtTnNcCy" ~80 chars - // 4096/80 = 51 shares ... with luck we might move the actual permissions to the value, saving ~15 chars - // 4096/64 = 64 shares ... still meh ... we can do better by using ints instead of strings for principals - // "user.oc.acl.u:100000" is pretty neat, but we can still do better: base64 encode the int - // "user.oc.acl.u:6Jqg" but base64 always has at least 4 chars, maybe hex is better for smaller numbers - // well use 4 chars in addition to the ace: "user.oc.acl.u:////" = 65535 -> 18 chars - // 4096/18 = 227 shares - // still ... ext attrs for this are not infinite scale ... - // so .. attach shares via fileid. - // /metadata//shares, similar to /files - // /metadata//shares/u///A:fdi:rwaDxtTnNcCy permissions as filename to keep them in the stat cache? - // - // whatever ... 50 shares is good enough. If more is needed we can delegate to the metadata - // if "user.oc.acl.M" is present look inside the metadata app. - // - if we cannot set an ace we might get an io error. - // in that case convert all shares to metadata and try to set "user.oc.acl.m" - // - // what about metadata like share creator, share time, expiry? - // - creator is same as owner, but can be set - // - share date, or abbreviated st is a unix timestamp - // - expiry is a unix timestamp - // - can be put inside the value - // - we need to reorder the fields: - // "user.oc.acl.:" -> "kv:t=:f=:p=:st=:c=:e=:pw=:n=" - // "user.oc.acl.:" -> "v1::::::::" - // or the first byte determines the format - // 0x00 = key value - // 0x01 = v1 ... - // // SharePrefix is the prefix for sharing related extended attributes sharePrefix string = "user.oc.acl." trashOriginPrefix string = "user.oc.o" @@ -750,209 +673,36 @@ func (fs *ocfs) AddGrant(ctx context.Context, ref *provider.Reference, g *provid return errors.Wrap(err, "ocfs: error resolving reference") } - e, err := fs.getACE(g) - if err != nil { - return err - } - - var attr string - if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP { - attr = sharePrefix + "g:" + e.Principal - } else { - attr = sharePrefix + "u:" + e.Principal - } - - if err := xattr.Set(np, attr, getValue(e)); err != nil { + e := ace.FromGrant(g) + principal, value := e.Marshal() + if err := xattr.Set(np, sharePrefix+principal, value); err != nil { return err } return fs.propagate(ctx, np) } -func getValue(e *ace) []byte { - // first byte will be replaced after converting to byte array - val := fmt.Sprintf("_t=%s:f=%s:p=%s", e.Type, e.Flags, e.Permissions) - b := []byte(val) - b[0] = 0 // indicate key value - return b -} - -func getACEPerm(set *provider.ResourcePermissions) (string, error) { - var b strings.Builder - - if set.Stat || set.InitiateFileDownload || set.ListContainer { - b.WriteString("r") - } - if set.InitiateFileUpload || set.Move { - b.WriteString("w") - } - if set.CreateContainer { - b.WriteString("a") - } - if set.Delete { - b.WriteString("d") - } - - // sharing - if set.AddGrant || set.RemoveGrant || set.UpdateGrant { - b.WriteString("C") - } - if set.ListGrants { - b.WriteString("c") - } - - // trash - if set.ListRecycle { - b.WriteString("u") - } - if set.RestoreRecycleItem { - b.WriteString("U") - } - if set.PurgeRecycle { - b.WriteString("P") - } - - // versions - if set.ListFileVersions { - b.WriteString("v") - } - if set.RestoreFileVersion { - b.WriteString("V") - } - - // quota - if set.GetQuota { - b.WriteString("q") - } - // TODO set quota permission? - // TODO GetPath - return b.String(), nil -} - -func (fs *ocfs) getACE(g *provider.Grant) (*ace, error) { - permissions, err := getACEPerm(g.Permissions) - if err != nil { - return nil, err - } - e := &ace{ - Principal: g.Grantee.Id.OpaqueId, - Permissions: permissions, - // TODO creator ... - Type: "A", - } - if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP { - e.Flags = "g" - } - return e, nil -} - -type ace struct { - //NFSv4 acls - Type string // t - Flags string // f - Principal string // im key - Permissions string // p - - // sharing specific - ShareTime int // s - Creator string // c - Expires int // e - Password string // w passWord TODO h = hash - Label string // l -} - -func unmarshalACE(v []byte) (*ace, error) { - // first byte indicates type of value - switch v[0] { - case 0: // = ':' separated key=value pairs - s := string(v[1:]) - return unmarshalKV(s) - default: - return nil, fmt.Errorf("unknown ace encoding") - } -} - -func unmarshalKV(s string) (*ace, error) { - e := &ace{} - r := csv.NewReader(strings.NewReader(s)) - r.Comma = ':' - r.Comment = 0 - r.FieldsPerRecord = -1 - r.LazyQuotes = false - r.TrimLeadingSpace = false - records, err := r.ReadAll() - if err != nil { - return nil, err - } - if len(records) != 1 { - return nil, fmt.Errorf("more than one row of ace kvs") - } - for i := range records[0] { - kv := strings.Split(records[0][i], "=") - switch kv[0] { - case "t": - e.Type = kv[1] - case "f": - e.Flags = kv[1] - case "p": - e.Permissions = kv[1] - case "s": - v, err := strconv.Atoi(kv[1]) - if err != nil { - return nil, err - } - e.ShareTime = v - case "c": - e.Creator = kv[1] - case "e": - v, err := strconv.Atoi(kv[1]) - if err != nil { - return nil, err - } - e.Expires = v - case "w": - e.Password = kv[1] - case "l": - e.Label = kv[1] - // TODO default ... log unknown keys? or add as opaque? hm we need that for tagged shares ... - } - } - return e, nil -} - -// Parse parses an acl string with the given delimiter (LongTextForm or ShortTextForm) -func getACEs(ctx context.Context, fsfn string, attrs []string) (entries []*ace, err error) { +// extractACEsFromAttrs reads ACEs in the list of attrs from the file +func extractACEsFromAttrs(ctx context.Context, fsfn string, attrs []string) (entries []*ace.ACE) { log := appctx.GetLogger(ctx) - entries = []*ace{} + entries = []*ace.ACE{} for i := range attrs { if strings.HasPrefix(attrs[i], sharePrefix) { - principal := attrs[i][len(sharePrefix):] var value []byte + var err error if value, err = xattr.Get(fsfn, attrs[i]); err != nil { log.Error().Err(err).Str("attr", attrs[i]).Msg("could not read attribute") continue } - var e *ace - if e, err = unmarshalACE(value); err != nil { - log.Error().Err(err).Str("attr", attrs[i]).Msg("could unmarshal ace") + var e *ace.ACE + principal := attrs[i][len(sharePrefix):] + if e, err = ace.Unmarshal(principal, value); err != nil { + log.Error().Err(err).Str("principal", principal).Str("attr", attrs[i]).Msg("could not unmarshal ace") continue } - e.Principal = principal[2:] - // check consistency of Flags and principal type - if strings.Contains(e.Flags, "g") { - if principal[:1] != "g" { - log.Error().Str("attr", attrs[i]).Interface("ace", e).Msg("inconsistent ace: expected group") - continue - } - } else { - if principal[:1] != "u" { - log.Error().Str("attr", attrs[i]).Interface("ace", e).Msg("inconsistent ace: expected user") - continue - } - } entries = append(entries, e) } } - return entries, nil + return } func (fs *ocfs) ListGrants(ctx context.Context, ref *provider.Reference) (grants []*provider.Grant, err error) { @@ -968,105 +718,17 @@ func (fs *ocfs) ListGrants(ctx context.Context, ref *provider.Reference) (grants } log.Debug().Interface("attrs", attrs).Msg("read attributes") - // filter attributes - var aces []*ace - if aces, err = getACEs(ctx, np, attrs); err != nil { - log.Error().Err(err).Msg("error getting aces") - return nil, err - } + + aces := extractACEsFromAttrs(ctx, np, attrs) grants = make([]*provider.Grant, 0, len(aces)) for i := range aces { - grantee := &provider.Grantee{ - // TODO lookup uid from principal - Id: &userpb.UserId{OpaqueId: aces[i].Principal}, - Type: fs.getGranteeType(aces[i]), - } - grants = append(grants, &provider.Grant{ - Grantee: grantee, - Permissions: fs.getGrantPermissionSet(aces[i].Permissions), - }) + grants = append(grants, aces[i].Grant()) } return grants, nil } -func (fs *ocfs) getGranteeType(e *ace) provider.GranteeType { - if strings.Contains(e.Flags, "g") { - return provider.GranteeType_GRANTEE_TYPE_GROUP - } - return provider.GranteeType_GRANTEE_TYPE_USER -} - -func (fs *ocfs) getGrantPermissionSet(mode string) *provider.ResourcePermissions { - p := &provider.ResourcePermissions{} - // r - if strings.Contains(mode, "r") { - p.Stat = true - p.InitiateFileDownload = true - p.ListContainer = true - } - // w - if strings.Contains(mode, "w") { - p.InitiateFileUpload = true - if p.InitiateFileDownload { - p.Move = true - } - } - //a - if strings.Contains(mode, "a") { - // TODO append data to file permission? - p.CreateContainer = true - } - //x - //if strings.Contains(mode, "x") { - // TODO execute file permission? - // TODO change directory permission? - //} - //d - if strings.Contains(mode, "d") { - p.Delete = true - } - //D ? - - // sharing - if strings.Contains(mode, "C") { - p.AddGrant = true - p.RemoveGrant = true - p.UpdateGrant = true - } - if strings.Contains(mode, "c") { - p.ListGrants = true - } - - // trash - if strings.Contains(mode, "u") { // u = undelete - p.ListRecycle = true - } - if strings.Contains(mode, "U") { - p.RestoreRecycleItem = true - } - if strings.Contains(mode, "P") { - p.PurgeRecycle = true - } - - // versions - if strings.Contains(mode, "v") { - p.ListFileVersions = true - } - if strings.Contains(mode, "V") { - p.RestoreFileVersion = true - } - - // ? - // TODO GetPath - if strings.Contains(mode, "q") { - p.GetQuota = true - } - // TODO set quota permission? - return p -} - func (fs *ocfs) RemoveGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) (err error) { var np string diff --git a/pkg/storage/utils/ace/ace.go b/pkg/storage/utils/ace/ace.go new file mode 100644 index 00000000000..734fab1a261 --- /dev/null +++ b/pkg/storage/utils/ace/ace.go @@ -0,0 +1,346 @@ +package ace + +import ( + "encoding/csv" + "fmt" + "strconv" + "strings" + + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" +) + +// ACE represents an Access Control Entry, mimicing NFSv4 ACLs +// +// The following is taken from the nfs4_acl man page, +// see https://linux.die.net/man/5/nfs4_acl: +// the extended attributes will look like this +// "user.oc.acl.:::" +// - *type* will be limited to A for now +// A: Allow - allow *principal* to perform actions requiring *permissions* +// In the future we can use: +// U: aUdit - log any attempted access by principal which requires +// permissions. +// L: aLarm - generate a system alarm at any attempted access by +// principal which requires permissions +// D for deny is not recommended +// - *flags* for now empty or g for group, no inheritance yet +// - d directory-inherit - newly-created subdirectories will inherit the +// ACE. +// - f file-inherit - newly-created files will inherit the ACE, minus its +// inheritance flags. Newly-created subdirectories +// will inherit the ACE; if directory-inherit is not +// also specified in the parent ACE, inherit-only will +// be added to the inherited ACE. +// - n no-propagate-inherit - newly-created subdirectories will inherit +// the ACE, minus its inheritance flags. +// - i inherit-only - the ACE is not considered in permissions checks, +// but it is heritable; however, the inherit-only +// flag is stripped from inherited ACEs. +// - *principal* a named user, group or special principal +// - the oidc sub@iss maps nicely to this +// - 'OWNER@', 'GROUP@', and 'EVERYONE@', which are, respectively, analogous to the POSIX user/group/other +// - *permissions* +// - r read-data (files) / list-directory (directories) +// - w write-data (files) / create-file (directories) +// - a append-data (files) / create-subdirectory (directories) +// - x execute (files) / change-directory (directories) +// - d delete - delete the file/directory. Some servers will allow a delete to occur if either this permission is set in the file/directory or if the delete-child permission is set in its parent directory. +// - D delete-child - remove a file or subdirectory from within the given directory (directories only) +// - t read-attributes - read the attributes of the file/directory. +// - T write-attributes - write the attributes of the file/directory. +// - n read-named-attributes - read the named attributes of the file/directory. +// - N write-named-attributes - write the named attributes of the file/directory. +// - c read-ACL - read the file/directory NFSv4 ACL. +// - C write-ACL - write the file/directory NFSv4 ACL. +// - o write-owner - change ownership of the file/directory. +// - y synchronize - allow clients to use synchronous I/O with the server. +// TODO implement OWNER@ as "user.oc.acl.A::OWNER@:rwaDxtTnNcCy" +// attribute names are limited to 255 chars by the linux kernel vfs, values to 64 kb +// ext3 extended attributes must fit inside a single filesystem block ... 4096 bytes +// that leaves us with "user.oc.acl.A::someonewithaslightlylongersubject@whateverissuer:rwaDxtTnNcCy" ~80 chars +// 4096/80 = 51 shares ... with luck we might move the actual permissions to the value, saving ~15 chars +// 4096/64 = 64 shares ... still meh ... we can do better by using ints instead of strings for principals +// "user.oc.acl.u:100000" is pretty neat, but we can still do better: base64 encode the int +// "user.oc.acl.u:6Jqg" but base64 always has at least 4 chars, maybe hex is better for smaller numbers +// well use 4 chars in addition to the ace: "user.oc.acl.u:////" = 65535 -> 18 chars +// 4096/18 = 227 shares +// still ... ext attrs for this are not infinite scale ... +// so .. attach shares via fileid. +// /metadata//shares, similar to /files +// /metadata//shares/u///A:fdi:rwaDxtTnNcCy permissions as filename to keep them in the stat cache? +// +// whatever ... 50 shares is good enough. If more is needed we can delegate to the metadata +// if "user.oc.acl.M" is present look inside the metadata app. +// - if we cannot set an ace we might get an io error. +// in that case convert all shares to metadata and try to set "user.oc.acl.m" +// +// what about metadata like share creator, share time, expiry? +// - creator is same as owner, but can be set +// - share date, or abbreviated st is a unix timestamp +// - expiry is a unix timestamp +// - can be put inside the value +// - we need to reorder the fields: +// "user.oc.acl.:" -> "kv:t=:f=:p=:st=:c=:e=:pw=:n=" +// "user.oc.acl.:" -> "v1::::::::" +// or the first byte determines the format +// 0x00 = key value +// 0x01 = v1 ... +type ACE struct { + //NFSv4 acls + _type string // t + flags string // f + principal string // im key + permissions string // p + + // sharing specific + shareTime int // s + creator string // c + expires int // e + password string // w passWord TODO h = hash + label string // l +} + +// FromGrant creates an ACE from a CS3 grant +func FromGrant(g *provider.Grant) *ACE { + e := &ACE{ + _type: "A", + permissions: getACEPerm(g.Permissions), + // TODO creator ... + } + if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP { + e.flags = "g" + e.principal = "g:" + g.Grantee.Id.OpaqueId + } else { + e.principal = "u:" + g.Grantee.Id.OpaqueId + } + return e +} + +// Principal returns the principal of the ACE, eg. `u:` or `g:` +func (e *ACE) Principal() string { + return e.principal +} + +// Marshal renders a principal and byte[] that can be used to persist the ACE as an extended attribute +func (e *ACE) Marshal() (string, []byte) { + // first byte will be replaced after converting to byte array + val := fmt.Sprintf("_t=%s:f=%s:p=%s", e._type, e.flags, e.permissions) + b := []byte(val) + b[0] = 0 // indicate key value + return e.principal, b +} + +// Unmarshal parses a principal string and byte[] into an ACE +func Unmarshal(principal string, v []byte) (e *ACE, err error) { + // first byte indicates type of value + switch v[0] { + case 0: // = ':' separated key=value pairs + s := string(v[1:]) + if e, err = unmarshalKV(s); err == nil { + e.principal = principal + } + // check consistency of Flags and principal type + if strings.Contains(e.flags, "g") { + if principal[:1] != "g" { + return nil, fmt.Errorf("inconsistent ace: expected group") + } + } else { + if principal[:1] != "u" { + return nil, fmt.Errorf("inconsistent ace: expected user") + } + } + default: + return nil, fmt.Errorf("unknown ace encoding") + } + return +} + +// Grant returns a CS3 grant +func (e *ACE) Grant() *provider.Grant { + return &provider.Grant{ + Grantee: &provider.Grantee{ + Id: &userpb.UserId{OpaqueId: e.principal}, + Type: e.granteeType(), + }, + Permissions: e.grantPermissionSet(), + } +} + +// granteeType returns the CS3 grantee type +func (e *ACE) granteeType() provider.GranteeType { + if strings.Contains(e.flags, "g") { + return provider.GranteeType_GRANTEE_TYPE_GROUP + } + return provider.GranteeType_GRANTEE_TYPE_USER +} + +// grantPermissionSet returns the set of CS3 resource permissions representing the ACE +func (e *ACE) grantPermissionSet() *provider.ResourcePermissions { + p := &provider.ResourcePermissions{} + // r + if strings.Contains(e.permissions, "r") { + p.Stat = true + p.InitiateFileDownload = true + p.ListContainer = true + } + // w + if strings.Contains(e.permissions, "w") { + p.InitiateFileUpload = true + if p.InitiateFileDownload { + p.Move = true + } + } + //a + if strings.Contains(e.permissions, "a") { + // TODO append data to file permission? + p.CreateContainer = true + } + //x + //if strings.Contains(e.Permissions, "x") { + // TODO execute file permission? + // TODO change directory permission? + //} + //d + if strings.Contains(e.permissions, "d") { + p.Delete = true + } + //D ? + + // sharing + if strings.Contains(e.permissions, "C") { + p.AddGrant = true + p.RemoveGrant = true + p.UpdateGrant = true + } + if strings.Contains(e.permissions, "c") { + p.ListGrants = true + } + + // trash + if strings.Contains(e.permissions, "u") { // u = undelete + p.ListRecycle = true + } + if strings.Contains(e.permissions, "U") { + p.RestoreRecycleItem = true + } + if strings.Contains(e.permissions, "P") { + p.PurgeRecycle = true + } + + // versions + if strings.Contains(e.permissions, "v") { + p.ListFileVersions = true + } + if strings.Contains(e.permissions, "V") { + p.RestoreFileVersion = true + } + + // ? + // TODO GetPath + if strings.Contains(e.permissions, "q") { + p.GetQuota = true + } + // TODO set quota permission? + return p +} + +func unmarshalKV(s string) (*ACE, error) { + e := &ACE{} + r := csv.NewReader(strings.NewReader(s)) + r.Comma = ':' + r.Comment = 0 + r.FieldsPerRecord = -1 + r.LazyQuotes = false + r.TrimLeadingSpace = false + records, err := r.ReadAll() + if err != nil { + return nil, err + } + if len(records) != 1 { + return nil, fmt.Errorf("more than one row of ace kvs") + } + for i := range records[0] { + kv := strings.Split(records[0][i], "=") + switch kv[0] { + case "t": + e._type = kv[1] + case "f": + e.flags = kv[1] + case "p": + e.permissions = kv[1] + case "s": + v, err := strconv.Atoi(kv[1]) + if err != nil { + return nil, err + } + e.shareTime = v + case "c": + e.creator = kv[1] + case "e": + v, err := strconv.Atoi(kv[1]) + if err != nil { + return nil, err + } + e.expires = v + case "w": + e.password = kv[1] + case "l": + e.label = kv[1] + // TODO default ... log unknown keys? or add as opaque? hm we need that for tagged shares ... + } + } + return e, nil +} + +func getACEPerm(set *provider.ResourcePermissions) string { + var b strings.Builder + + if set.Stat || set.InitiateFileDownload || set.ListContainer { + b.WriteString("r") + } + if set.InitiateFileUpload || set.Move { + b.WriteString("w") + } + if set.CreateContainer { + b.WriteString("a") + } + if set.Delete { + b.WriteString("d") + } + + // sharing + if set.AddGrant || set.RemoveGrant || set.UpdateGrant { + b.WriteString("C") + } + if set.ListGrants { + b.WriteString("c") + } + + // trash + if set.ListRecycle { + b.WriteString("u") + } + if set.RestoreRecycleItem { + b.WriteString("U") + } + if set.PurgeRecycle { + b.WriteString("P") + } + + // versions + if set.ListFileVersions { + b.WriteString("v") + } + if set.RestoreFileVersion { + b.WriteString("V") + } + + // quota + if set.GetQuota { + b.WriteString("q") + } + // TODO set quota permission? + // TODO GetPath + return b.String() +}