diff --git a/changelog/unreleased/lightweigh-accounts.md b/changelog/unreleased/lightweigh-accounts.md new file mode 100644 index 00000000000..7984bbff28b --- /dev/null +++ b/changelog/unreleased/lightweigh-accounts.md @@ -0,0 +1,8 @@ +Enhancement: Revamp lightweigth accounts + +Re-implements the lighweight account scope check, making +it more efficient. +Also, the ACLs for the EOS storage driver for the lw +accounts are set atomically. + +https://github.com/cs3org/reva/pull/3348 \ No newline at end of file diff --git a/internal/grpc/interceptors/auth/scope.go b/internal/grpc/interceptors/auth/scope.go index 1bb5581ab77..78d481ad36a 100644 --- a/internal/grpc/interceptors/auth/scope.go +++ b/internal/grpc/interceptors/auth/scope.go @@ -20,6 +20,7 @@ package auth import ( "context" + "path/filepath" "strings" "time" @@ -78,77 +79,136 @@ func expandAndVerifyScope(ctx context.Context, req interface{}, tokenScope map[s return nil } - case strings.HasPrefix(k, "lightweight"): - if err = resolveLightweightScope(ctx, ref, tokenScope[k], user, client, mgr); err == nil { - return nil - } } log.Err(err).Msgf("error resolving reference %s under scope %+v", ref.String(), k) } - } else if ref, ok := extractShareRef(req); ok { - // It's a share ref - // The request might be coming from a share created for a lightweight account - // after the token was minted. - log.Info().Msgf("resolving share reference against received shares to verify token scope %+v", ref.String()) - for k := range tokenScope { - if strings.HasPrefix(k, "lightweight") { - // Check if this ID is cached - key := "lw:" + user.Id.OpaqueId + scopeDelimiter + ref.GetId().OpaqueId - if _, err := scopeExpansionCache.Get(key); err == nil { - return nil - } + } - shares, err := client.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{}) - if err != nil || shares.Status.Code != rpc.Code_CODE_OK { - log.Warn().Err(err).Msg("error listing received shares") - continue - } - for _, s := range shares.Shares { - shareKey := "lw:" + user.Id.OpaqueId + scopeDelimiter + s.Share.Id.OpaqueId - _ = scopeExpansionCache.SetWithExpire(shareKey, nil, scopeCacheExpiration*time.Second) - - if ref.GetId() != nil && ref.GetId().OpaqueId == s.Share.Id.OpaqueId { - return nil - } - if key := ref.GetKey(); key != nil && (utils.UserEqual(key.Owner, s.Share.Owner) || utils.UserEqual(key.Owner, s.Share.Creator)) && - utils.ResourceIDEqual(key.ResourceId, s.Share.ResourceId) && utils.GranteeEqual(key.Grantee, s.Share.Grantee) { - return nil - } - } - } - } + if checkLightweightScope(ctx, req, tokenScope, client) { + return nil } return errtypes.PermissionDenied("access to resource not allowed within the assigned scope") } -func resolveLightweightScope(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, user *userpb.User, client gateway.GatewayAPIClient, mgr token.Manager) error { - // Check if this ref is cached - key := "lw:" + user.Id.OpaqueId + scopeDelimiter + getRefKey(ref) - if _, err := scopeExpansionCache.Get(key); err == nil { - return nil +func hasLightweightScope(tokenScope map[string]*authpb.Scope) bool { + for scope := range tokenScope { + if strings.HasPrefix(scope, "lightweight") { + return true + } } + return false +} - shares, err := client.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{}) - if err != nil || shares.Status.Code != rpc.Code_CODE_OK { - return errtypes.InternalError("error listing received shares") +func checkLightweightScope(ctx context.Context, req interface{}, tokenScope map[string]*authpb.Scope, client gateway.GatewayAPIClient) bool { + if !hasLightweightScope(tokenScope) { + return false } - for _, share := range shares.Shares { - shareKey := "lw:" + user.Id.OpaqueId + scopeDelimiter + resourceid.OwnCloudResourceIDWrap(share.Share.ResourceId) - _ = scopeExpansionCache.SetWithExpire(shareKey, nil, scopeCacheExpiration*time.Second) + switch r := req.(type) { + // Viewer role + case *registry.GetStorageProvidersRequest: + return true + case *provider.StatRequest: + return true + case *provider.ListContainerRequest: + return hasPermissions(ctx, client, r.GetRef(), &provider.ResourcePermissions{ + ListContainer: true, + }) + case *provider.InitiateFileDownloadRequest: + return hasPermissions(ctx, client, r.GetRef(), &provider.ResourcePermissions{ + InitiateFileDownload: true, + }) + case *appprovider.OpenInAppRequest: + return hasPermissions(ctx, client, &provider.Reference{ResourceId: r.ResourceInfo.Id}, &provider.ResourcePermissions{ + InitiateFileDownload: true, + }) + case *gateway.OpenInAppRequest: + return hasPermissions(ctx, client, r.GetRef(), &provider.ResourcePermissions{ + InitiateFileDownload: true, + }) - if ref.ResourceId != nil && utils.ResourceIDEqual(share.Share.ResourceId, ref.ResourceId) { - return nil + // Editor role + case *provider.CreateContainerRequest: + parent, err := parentOfResource(ctx, client, r.GetRef()) + if err != nil { + return false + } + return hasPermissions(ctx, client, parent, &provider.ResourcePermissions{ + CreateContainer: true, + }) + case *provider.TouchFileRequest: + parent, err := parentOfResource(ctx, client, r.GetRef()) + if err != nil { + return false } - if ok, err := checkIfNestedResource(ctx, ref, share.Share.ResourceId, client, mgr); err == nil && ok { - _ = scopeExpansionCache.SetWithExpire(key, nil, scopeCacheExpiration*time.Second) - return nil + return hasPermissions(ctx, client, parent, &provider.ResourcePermissions{ + InitiateFileDownload: true, + }) + case *provider.DeleteRequest: + return hasPermissions(ctx, client, r.GetRef(), &provider.ResourcePermissions{ + InitiateFileDownload: true, + }) + case *provider.MoveRequest: + return hasPermissions(ctx, client, r.Source, &provider.ResourcePermissions{ + InitiateFileDownload: true, + }) && hasPermissions(ctx, client, r.Destination, &provider.ResourcePermissions{ + InitiateFileUpload: true, + }) + case *provider.InitiateFileUploadRequest: + parent, err := parentOfResource(ctx, client, r.GetRef()) + if err != nil { + return false } + return hasPermissions(ctx, client, parent, &provider.ResourcePermissions{ + InitiateFileUpload: true, + }) } - return errtypes.PermissionDenied("request is not for a nested resource") + return false +} + +func parentOfResource(ctx context.Context, client gateway.GatewayAPIClient, ref *provider.Reference) (*provider.Reference, error) { + if utils.IsAbsolutePathReference(ref) { + parent := filepath.Dir(ref.GetPath()) + info, err := stat(ctx, client, &provider.Reference{Path: parent}) + if err != nil { + return nil, err + } + return &provider.Reference{ResourceId: info.Id}, nil + } + + info, err := stat(ctx, client, ref) + if err != nil { + return nil, err + } + return &provider.Reference{ResourceId: info.ParentId}, nil +} + +func stat(ctx context.Context, client gateway.GatewayAPIClient, ref *provider.Reference) (*provider.ResourceInfo, error) { + statRes, err := client.Stat(ctx, &provider.StatRequest{ + Ref: ref, + }) + + switch { + case err != nil: + return nil, err + case statRes.Status.Code == rpc.Code_CODE_NOT_FOUND: + return nil, errtypes.NotFound(statRes.Status.Message) + case statRes.Status.Code != rpc.Code_CODE_OK: + return nil, errtypes.InternalError(statRes.Status.Message) + } + + return statRes.Info, nil +} + +func hasPermissions(ctx context.Context, client gateway.GatewayAPIClient, ref *provider.Reference, permissionSet *provider.ResourcePermissions) bool { + info, err := stat(ctx, client, ref) + if err != nil { + return false + } + return utils.HasPermissions(info.PermissionSet, permissionSet) } func resolvePublicShare(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, client gateway.GatewayAPIClient, mgr token.Manager) error { @@ -329,16 +389,6 @@ func extractRef(req interface{}, tokenScope map[string]*authpb.Scope) (*provider return nil, false } -func extractShareRef(req interface{}) (*collaboration.ShareReference, bool) { - switch v := req.(type) { - case *collaboration.GetReceivedShareRequest: - return v.GetRef(), true - case *collaboration.UpdateReceivedShareRequest: - return &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{Id: v.GetShare().GetShare().GetId()}}, true - } - return nil, false -} - func getRefKey(ref *provider.Reference) string { if ref.Path != "" { return ref.Path diff --git a/pkg/auth/scope/lightweight.go b/pkg/auth/scope/lightweight.go index 5ebc8d22252..cc314ce1dbe 100644 --- a/pkg/auth/scope/lightweight.go +++ b/pkg/auth/scope/lightweight.go @@ -56,11 +56,13 @@ func checkLightweightPath(path string) bool { "/ocs/v1.php/cloud/user", "/remote.php/webdav", "/remote.php/dav/files", + "/thumbnails", "/app/open", "/app/new", "/archiver", "/dataprovider", "/data", + "/app/open", } for _, p := range paths { if strings.HasPrefix(path, p) { diff --git a/pkg/eosclient/eosbinary/eosbinary.go b/pkg/eosclient/eosbinary/eosbinary.go index 0f80ec55c66..2a56a863c48 100644 --- a/pkg/eosclient/eosbinary/eosbinary.go +++ b/pkg/eosclient/eosbinary/eosbinary.go @@ -45,7 +45,6 @@ import ( const ( versionPrefix = ".sys.v#." - lwShareAttrKey = "reva.lwshare" userACLEvalKey = "eval.useracl" favoritesKey = "http://owncloud.org/ns/favorite" ) @@ -280,30 +279,6 @@ func (c *Client) AddACL(ctx context.Context, auth, rootAuth eosclient.Authorizat return err } - if a.Type == acl.TypeLightweight { - sysACL := "" - aclStr, ok := finfo.Attrs["sys."+lwShareAttrKey] - if ok { - acls, err := acl.Parse(aclStr, acl.ShortTextForm) - if err != nil { - return err - } - err = acls.SetEntry(a.Type, a.Qualifier, a.Permissions) - if err != nil { - return err - } - sysACL = acls.Serialize() - } else { - sysACL = a.CitrineSerialize() - } - sysACLAttr := &eosclient.Attribute{ - Type: eosclient.SystemAttr, - Key: lwShareAttrKey, - Val: sysACL, - } - return c.SetAttr(ctx, auth, sysACLAttr, false, finfo.IsDir, path) - } - sysACL := a.CitrineSerialize() args := []string{"acl", "--sys"} if finfo.IsDir { @@ -330,30 +305,6 @@ func (c *Client) RemoveACL(ctx context.Context, auth, rootAuth eosclient.Authori return err } - if a.Type == acl.TypeLightweight { - sysACL := "" - aclStr, ok := finfo.Attrs["sys."+lwShareAttrKey] - if ok { - acls, err := acl.Parse(aclStr, acl.ShortTextForm) - if err != nil { - return err - } - acls.DeleteEntry(a.Type, a.Qualifier) - if err != nil { - return err - } - sysACL = acls.Serialize() - } else { - sysACL = a.CitrineSerialize() - } - sysACLAttr := &eosclient.Attribute{ - Type: eosclient.SystemAttr, - Key: lwShareAttrKey, - Val: sysACL, - } - return c.SetAttr(ctx, auth, sysACLAttr, false, finfo.IsDir, path) - } - sysACL := a.CitrineSerialize() args := []string{"acl", "--sys"} if finfo.IsDir { @@ -645,6 +596,34 @@ func (c *Client) GetAttr(ctx context.Context, auth eosclient.Authorization, key, return attr, nil } +// GetAttrs returns all the attributes of a resource +func (c *Client) GetAttrs(ctx context.Context, auth eosclient.Authorization, path string) ([]*eosclient.Attribute, error) { + info, err := c.getRawFileInfoByPath(ctx, auth, path) + if err != nil { + return nil, err + } + if !info.IsDir { + path = getVersionFolder(path) + } + + args := []string{"attr", "ls", path} + attrOut, _, err := c.executeEOS(ctx, args, auth) + if err != nil { + return nil, err + } + + attrsStr := strings.Split(attrOut, "\n") + attrs := make([]*eosclient.Attribute, 0, len(attrsStr)) + for _, line := range attrsStr { + attr, err := deserializeAttribute(line) + if err != nil { + return nil, err + } + attrs = append(attrs, attr) + } + return attrs, nil +} + func deserializeAttribute(attrStr string) (*eosclient.Attribute, error) { // the string is in the form sys.forced.checksum="adler" keyValue := strings.SplitN(strings.TrimSpace(attrStr), "=", 2) // keyValue = ["sys.forced.checksum", "\"adler\""] @@ -1222,20 +1201,6 @@ func (c *Client) mapToFileInfo(ctx context.Context, kv, attrs map[string]string, } } - // Read lightweight ACLs recognized by the sys.reva.lwshare attr - if lwACLStr, ok := attrs["sys."+lwShareAttrKey]; ok { - lwAcls, err := acl.Parse(lwACLStr, acl.ShortTextForm) - if err != nil { - return nil, err - } - for _, e := range lwAcls.Entries { - err = sysACL.SetEntry(e.Type, e.Qualifier, e.Permissions) - if err != nil { - return nil, err - } - } - } - // Read the favorite attr if parseFavoriteKey { parseAndSetFavoriteAttr(ctx, attrs) diff --git a/pkg/eosclient/eosclient.go b/pkg/eosclient/eosclient.go index b62c4134581..c9d52d4d874 100644 --- a/pkg/eosclient/eosclient.go +++ b/pkg/eosclient/eosclient.go @@ -39,6 +39,7 @@ type EOSClient interface { SetAttr(ctx context.Context, auth Authorization, attr *Attribute, errorIfExists, recursive bool, path string) error UnsetAttr(ctx context.Context, auth Authorization, attr *Attribute, recursive bool, path string) error GetAttr(ctx context.Context, auth Authorization, key, path string) (*Attribute, error) + GetAttrs(ctx context.Context, auth Authorization, path string) ([]*Attribute, error) GetQuota(ctx context.Context, username string, rootAuth Authorization, path string) (*QuotaInfo, error) SetQuota(ctx context.Context, rooAuth Authorization, info *SetQuotaInfo) error Touch(ctx context.Context, auth Authorization, path string) error diff --git a/pkg/eosclient/eosgrpc/eosgrpc.go b/pkg/eosclient/eosgrpc/eosgrpc.go index 47bd3c4a7f1..2c4f47c2696 100644 --- a/pkg/eosclient/eosgrpc/eosgrpc.go +++ b/pkg/eosclient/eosgrpc/eosgrpc.go @@ -49,7 +49,6 @@ import ( const ( versionPrefix = ".sys.v#." - // lwShareAttrKey = "reva.lwshare" ) const ( @@ -629,6 +628,25 @@ func (c *Client) GetAttr(ctx context.Context, auth eosclient.Authorization, key, return nil, errtypes.NotFound(fmt.Sprintf("key %s not found", key)) } +// GetAttrs returns all the attributes of a resource +func (c *Client) GetAttrs(ctx context.Context, auth eosclient.Authorization, path string) ([]*eosclient.Attribute, error) { + info, err := c.GetFileInfoByPath(ctx, auth, path) + if err != nil { + return nil, err + } + + attrs := make([]*eosclient.Attribute, 0, len(info.Attrs)) + for k, v := range info.Attrs { + attr, err := getAttribute(k, v) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("eosgrpc: cannot parse attribute key=%s value=%s", k, v)) + } + attrs = append(attrs, attr) + } + + return attrs, nil +} + func getAttribute(key, val string) (*eosclient.Attribute, error) { // key is in the form sys.forced.checksum type2key := strings.SplitN(key, ".", 2) // type2key = ["sys", "forced.checksum"] @@ -1238,8 +1256,10 @@ func (c *Client) List(ctx context.Context, auth eosclient.Authorization, dpath s // Read reads a file from the mgm and returns a handle to read it // This handle could be directly the body of the response or a local tmp file -// returning a handle to the body is nice, yet it gives less control on the transaction -// itself, e.g. strange timeouts or TCP issues may be more difficult to trace +// +// returning a handle to the body is nice, yet it gives less control on the transaction +// itself, e.g. strange timeouts or TCP issues may be more difficult to trace +// // Let's consider this experimental for the moment, maybe I'll like to add a config // parameter to choose between the two behaviours func (c *Client) Read(ctx context.Context, auth eosclient.Authorization, path string) (io.ReadCloser, error) { diff --git a/pkg/storage/utils/eosfs/eosfs.go b/pkg/storage/utils/eosfs/eosfs.go index cb0fadc5fe2..e66e6a7b363 100644 --- a/pkg/storage/utils/eosfs/eosfs.go +++ b/pkg/storage/utils/eosfs/eosfs.go @@ -27,6 +27,7 @@ import ( "os" "os/exec" "path" + "path/filepath" "regexp" "strconv" "strings" @@ -63,6 +64,7 @@ import ( const ( refTargetAttrKey = "reva.target" + lwShareAttrKey = "reva.lwshare" ) const ( @@ -1001,15 +1003,28 @@ func (fs *eosfs) AddGrant(ctx context.Context, ref *provider.Reference, g *provi return err } - // position where put the ACL - position := eosclient.StartPosition - eosACL, err := fs.getEosACL(ctx, g) if err != nil { return err } - err = fs.c.AddACL(ctx, auth, rootAuth, fn, position, eosACL) + if eosACL.Type == acl.TypeLightweight { + // The ACLs for a lightweight are not understandable by EOS + // directly, but only from reva. So we have to store them + // in an xattr named sys.reva.lwshare., with value + // the permissions. + attr := &eosclient.Attribute{ + Type: SystemAttr, + Key: fmt.Sprintf("%s.%s", lwShareAttrKey, eosACL.Qualifier), + Val: eosACL.Permissions, + } + if err := fs.c.SetAttr(ctx, rootAuth, attr, false, true, fn); err != nil { + return errors.Wrap(err, "eosfs: error adding acl for lightweight account") + } + return nil + } + + err = fs.c.AddACL(ctx, auth, rootAuth, fn, eosclient.StartPosition, eosACL) if err != nil { return errors.Wrap(err, "eosfs: error adding acl") } @@ -1087,45 +1102,29 @@ func (fs *eosfs) getEosACL(ctx context.Context, g *provider.Grant) (*acl.Entry, } func (fs *eosfs) RemoveGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error { - eosACLType, err := grants.GetACLType(g.Grantee.Type) + fn, auth, err := fs.resolveRefAndGetAuth(ctx, ref) if err != nil { return err } - var recipient string - if eosACLType == acl.TypeUser { - // if the grantee is a lightweight account, we need to set it accordingly - if g.Grantee.GetUserId().Type == userpb.UserType_USER_TYPE_LIGHTWEIGHT || - g.Grantee.GetUserId().Type == userpb.UserType_USER_TYPE_FEDERATED { - eosACLType = acl.TypeLightweight - recipient = g.Grantee.GetUserId().OpaqueId - } else { - // since EOS Citrine ACLs are stored with uid, we need to convert username to uid - auth, err := fs.getUIDGateway(ctx, g.Grantee.GetUserId()) - if err != nil { - return err - } - recipient = auth.Role.UID - } - } else { - recipient = g.Grantee.GetGroupId().OpaqueId - } - - eosACL := &acl.Entry{ - Qualifier: recipient, - Type: eosACLType, - } - - fn, auth, err := fs.resolveRefAndGetAuth(ctx, ref) + rootAuth, err := fs.getRootAuth(ctx) if err != nil { return err } - rootAuth, err := fs.getRootAuth(ctx) + eosACL, err := fs.getEosACL(ctx, g) if err != nil { return err } + if eosACL.Type == acl.TypeLightweight { + attr := &eosclient.Attribute{} + if err := fs.c.UnsetAttr(ctx, rootAuth, attr, true, fn); err != nil { + return errors.Wrap(err, "eosfs: error removing acl for lightweight account") + } + return nil + } + err = fs.c.RemoveACL(ctx, auth, rootAuth, fn, eosACL) if err != nil { return errors.Wrap(err, "eosfs: error removing acl") @@ -1137,19 +1136,9 @@ func (fs *eosfs) UpdateGrant(ctx context.Context, ref *provider.Reference, g *pr return fs.AddGrant(ctx, ref, g) } -func (fs *eosfs) ListGrants(ctx context.Context, ref *provider.Reference) ([]*provider.Grant, error) { - fn, auth, err := fs.resolveRefAndGetAuth(ctx, ref) - if err != nil { - return nil, err - } - - acls, err := fs.c.ListACLs(ctx, auth, fn) - if err != nil { - return nil, err - } - - grantList := []*provider.Grant{} - for _, a := range acls { +func (fs *eosfs) convertACLsToGrants(ctx context.Context, acls *acl.ACLs) ([]*provider.Grant, error) { + res := make([]*provider.Grant, 0, len(acls.Entries)) + for _, a := range acls.Entries { var grantee *provider.Grantee switch { case a.Type == acl.TypeUser: @@ -1163,24 +1152,74 @@ func (fs *eosfs) ListGrants(ctx context.Context, ref *provider.Reference) ([]*pr Id: &provider.Grantee_UserId{UserId: qualifier}, Type: grants.GetGranteeType(a.Type), } - case a.Type == acl.TypeLightweight: - a.Type = acl.TypeUser - grantee = &provider.Grantee{ - Id: &provider.Grantee_UserId{UserId: &userpb.UserId{OpaqueId: a.Qualifier}}, - Type: grants.GetGranteeType(a.Type), - } - default: + case a.Type == acl.TypeGroup: grantee = &provider.Grantee{ Id: &provider.Grantee_GroupId{GroupId: &grouppb.GroupId{OpaqueId: a.Qualifier}}, Type: grants.GetGranteeType(a.Type), } + default: + return nil, errtypes.InternalError(fmt.Sprintf("eosfs: acl type %s not recognised", a.Type)) } - - grantList = append(grantList, &provider.Grant{ + res = append(res, &provider.Grant{ Grantee: grantee, Permissions: grants.GetGrantPermissionSet(a.Permissions), }) } + return res, nil +} + +func isSysACLs(a *eosclient.Attribute) bool { + return a.Type == SystemAttr && a.Key == "sys" +} + +func isLightweightACL(a *eosclient.Attribute) bool { + return a.Type == SystemAttr && strings.HasPrefix(a.Key, lwShareAttrKey) +} + +func parseLightweightACL(a *eosclient.Attribute) *provider.Grant { + qualifier := strings.TrimPrefix(a.Key, lwShareAttrKey+".") + return &provider.Grant{ + Grantee: &provider.Grantee{ + Id: &provider.Grantee_UserId{UserId: &userpb.UserId{ + // FIXME: idp missing, maybe get the user_id from the user provider? + Type: userpb.UserType_USER_TYPE_LIGHTWEIGHT, + OpaqueId: qualifier, + }}, + Type: grants.GetGranteeType(acl.TypeLightweight), + }, + Permissions: grants.GetGrantPermissionSet(a.Val), + } +} + +func (fs *eosfs) ListGrants(ctx context.Context, ref *provider.Reference) ([]*provider.Grant, error) { + fn, auth, err := fs.resolveRefAndGetAuth(ctx, ref) + if err != nil { + return nil, err + } + + attrs, err := fs.c.GetAttrs(ctx, auth, fn) + if err != nil { + return nil, err + } + + grantList := []*provider.Grant{} + for _, a := range attrs { + switch { + case isSysACLs(a): + // EOS ACLs + acls, err := acl.Parse(a.Val, acl.ShortTextForm) + if err != nil { + return nil, err + } + grants, err := fs.convertACLsToGrants(ctx, acls) + if err != nil { + return nil, err + } + grantList = append(grantList, grants...) + case isLightweightACL(a): + grantList = append(grantList, parseLightweightACL(a)) + } + } return grantList, nil } @@ -2053,23 +2092,17 @@ func (fs *eosfs) permissionSet(ctx context.Context, eosFileInfo *eosclient.FileI } } - if owner != nil && u.Id.OpaqueId == owner.OpaqueId && u.Id.Idp == owner.Idp { - // The logged-in user is the owner but we may be impersonating them - // on behalf of a public share accessor. - - if u.Opaque != nil { - if publicShare, ok := u.Opaque.Map["public-share-role"]; ok { - if string(publicShare.Value) == "editor" { - return conversions.NewEditorRole().CS3ResourcePermissions() - } else if string(publicShare.Value) == "uploader" { - return conversions.NewUploaderRole().CS3ResourcePermissions() - } - // Default to viewer role - return conversions.NewViewerRole().CS3ResourcePermissions() - } + if role, ok := utils.HasPublicShareRole(u); ok { + switch role { + case "editor": + return conversions.NewEditorRole().CS3ResourcePermissions() + case "uploader": + return conversions.NewUploaderRole().CS3ResourcePermissions() } + return conversions.NewViewerRole().CS3ResourcePermissions() + } - // owner has all permissions + if utils.UserEqual(u.Id, owner) { return conversions.NewManagerRole().CS3ResourcePermissions() } @@ -2087,25 +2120,71 @@ func (fs *eosfs) permissionSet(ctx context.Context, eosFileInfo *eosclient.FileI } var perm provider.ResourcePermissions - for _, e := range eosFileInfo.SysACL.Entries { - var userInGroup bool - if e.Type == acl.TypeGroup { - for _, g := range u.Groups { - if e.Qualifier == g { - userInGroup = true - break - } - } + // as the lightweight acl are stored as normal attrs, + // we need to add them in the sysacl entries + + for k, v := range eosFileInfo.Attrs { + if e, ok := attrForLightweightACL(k, v); ok { + eosFileInfo.SysACL.Entries = append(eosFileInfo.SysACL.Entries, e) } + } + + userGroupsSet := makeSet(u.Groups) + + for _, e := range eosFileInfo.SysACL.Entries { + userInGroup := e.Type == acl.TypeGroup && userGroupsSet.in(strings.ToLower(e.Qualifier)) if (e.Type == acl.TypeUser && e.Qualifier == auth.Role.UID) || (e.Type == acl.TypeLightweight && e.Qualifier == u.Id.OpaqueId) || userInGroup { mergePermissions(&perm, grants.GetGrantPermissionSet(e.Permissions)) } } + // for normal files, we need to inherit also the lw acls + // from the parent folder, as these, when creating a new + // file are not inherited + + if utils.UserIsLightweight(u) && !eosFileInfo.IsDir { + if parentPath, err := fs.unwrap(ctx, filepath.Dir(eosFileInfo.File)); err == nil { + if parent, err := fs.GetMD(ctx, &provider.Reference{Path: parentPath}, nil); err == nil { + mergePermissions(&perm, parent.PermissionSet) + } + } + } + return &perm } +func attrForLightweightACL(k, v string) (*acl.Entry, bool) { + ok := strings.HasPrefix(k, "sys."+lwShareAttrKey) + if !ok { + return nil, false + } + + qualifier := strings.TrimPrefix(k, fmt.Sprintf("sys.%s.", lwShareAttrKey)) + + attr := &acl.Entry{ + Type: acl.TypeLightweight, + Qualifier: qualifier, + Permissions: v, + } + return attr, true +} + +type groupSet map[string]struct{} + +func makeSet(lst []string) groupSet { + s := make(map[string]struct{}, len(lst)) + for _, e := range lst { + s[e] = struct{}{} + } + return s +} + +func (s groupSet) in(group string) bool { + _, ok := s[group] + return ok +} + func mergePermissions(l *provider.ResourcePermissions, r *provider.ResourcePermissions) { l.AddGrant = l.AddGrant || r.AddGrant l.CreateContainer = l.CreateContainer || r.CreateContainer diff --git a/pkg/storage/utils/grants/grants.go b/pkg/storage/utils/grants/grants.go index 72dbdc31dbf..f7185ba8de5 100644 --- a/pkg/storage/utils/grants/grants.go +++ b/pkg/storage/utils/grants/grants.go @@ -124,7 +124,7 @@ func GetACLType(gt provider.GranteeType) (string, error) { // GetGranteeType returns the grantee type from a char func GetGranteeType(aclType string) provider.GranteeType { switch aclType { - case acl.TypeUser: + case acl.TypeUser, acl.TypeLightweight: return provider.GranteeType_GRANTEE_TYPE_USER case acl.TypeGroup: return provider.GranteeType_GRANTEE_TYPE_GROUP diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 54033e3386c..be4fedf7dbf 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -27,6 +27,7 @@ import ( "os/user" "path" "path/filepath" + "reflect" "regexp" "strings" "time" @@ -354,3 +355,37 @@ func GetViewMode(viewMode string) gateway.OpenInAppRequest_ViewMode { return gateway.OpenInAppRequest_VIEW_MODE_INVALID } } + +// HasPublicShareRole return true if the user has a public share role. +// If yes, the string is the type of role, viewer, editor or uploader +func HasPublicShareRole(u *userpb.User) (string, bool) { + if u.Opaque == nil { + return "", false + } + if publicShare, ok := u.Opaque.Map["public-share-role"]; ok { + return string(publicShare.Value), true + } + return "", false +} + +// HasPermissions returns true if all permissions defined in the stuict toCheck +// are set in the target +func HasPermissions(target, toCheck *provider.ResourcePermissions) bool { + targetStruct := reflect.ValueOf(target).Elem() + toCheckStruct := reflect.ValueOf(toCheck).Elem() + + for i := 0; i < toCheckStruct.NumField(); i++ { + fieldToCheck := toCheckStruct.Field(i) + if fieldToCheck.Kind() == reflect.Bool && fieldToCheck.Bool() && !targetStruct.Field(i).Bool() { + return false + } + } + return true +} + +// UserIsLightweight returns true if the user is a lightweith +// or federated account +func UserIsLightweight(u *userpb.User) bool { + return u.Id.Type == userpb.UserType_USER_TYPE_FEDERATED || + u.Id.Type == userpb.UserType_USER_TYPE_LIGHTWEIGHT +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 29bbf661124..091e8463ead 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -207,3 +207,72 @@ func TestParseStorageSpaceReference(t *testing.T) { } } } + +func TestHasPermissions(t *testing.T) { + tests := []struct { + name string + target *provider.ResourcePermissions + toCheck *provider.ResourcePermissions + expected bool + }{ + { + name: "both empty", + target: &provider.ResourcePermissions{}, + toCheck: &provider.ResourcePermissions{}, + expected: true, + }, + { + name: "empty target", + target: &provider.ResourcePermissions{}, + toCheck: &provider.ResourcePermissions{ + AddGrant: true, + }, + expected: false, + }, + { + name: "empty to_check", + target: &provider.ResourcePermissions{ + AddGrant: true, + }, + toCheck: &provider.ResourcePermissions{}, + expected: true, + }, + { + name: "to_check is a subset", + target: &provider.ResourcePermissions{ + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + }, + toCheck: &provider.ResourcePermissions{ + CreateContainer: true, + GetPath: true, + }, + expected: true, + }, + { + name: "to_check contains permissions to in target", + target: &provider.ResourcePermissions{ + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + }, + toCheck: &provider.ResourcePermissions{ + CreateContainer: true, + GetPath: true, + Move: true, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if res := HasPermissions(tt.target, tt.toCheck); res != tt.expected { + t.Fatalf("got unexpected result: target=%+v to_check=%+v res=%+v expected=%+v", tt.target, tt.toCheck, res, tt.expected) + } + }) + } +}