github.com/cs3org/reva/v2@v2.27.7/pkg/cbox/storage/eoswrapper/eoswrapper.go (about)

     1  // Copyright 2018-2021 CERN
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // In applying this license, CERN does not waive the privileges and immunities
    16  // granted to it by virtue of its status as an Intergovernmental Organization
    17  // or submit itself to any jurisdiction.
    18  
    19  package eoswrapper
    20  
    21  import (
    22  	"bytes"
    23  	"context"
    24  	"io"
    25  	"path"
    26  	"strings"
    27  	"text/template"
    28  
    29  	"github.com/Masterminds/sprig"
    30  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    31  	"github.com/mitchellh/mapstructure"
    32  	"github.com/pkg/errors"
    33  	"github.com/rs/zerolog"
    34  
    35  	ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
    36  	"github.com/cs3org/reva/v2/pkg/errtypes"
    37  	"github.com/cs3org/reva/v2/pkg/events"
    38  	"github.com/cs3org/reva/v2/pkg/storage"
    39  	"github.com/cs3org/reva/v2/pkg/storage/fs/registry"
    40  	"github.com/cs3org/reva/v2/pkg/storage/utils/eosfs"
    41  	"github.com/cs3org/reva/v2/pkg/storagespace"
    42  	"github.com/cs3org/reva/v2/pkg/utils"
    43  )
    44  
    45  func init() {
    46  	registry.Register("eoswrapper", New)
    47  }
    48  
    49  const (
    50  	eosProjectsNamespace = "/eos/project"
    51  
    52  	// We can use a regex for these, but that might have inferior performance
    53  	projectSpaceGroupsPrefix      = "cernbox-project-"
    54  	projectSpaceAdminGroupsSuffix = "-admins"
    55  )
    56  
    57  type wrapper struct {
    58  	storage.FS
    59  	conf            *eosfs.Config
    60  	mountIDTemplate *template.Template
    61  }
    62  
    63  func parseConfig(m map[string]interface{}) (*eosfs.Config, string, error) {
    64  	c := &eosfs.Config{}
    65  	if err := mapstructure.Decode(m, c); err != nil {
    66  		err = errors.Wrap(err, "error decoding conf")
    67  		return nil, "", err
    68  	}
    69  
    70  	// default to version invariance if not configured
    71  	if _, ok := m["version_invariant"]; !ok {
    72  		c.VersionInvariant = true
    73  	}
    74  
    75  	// allow recycle operations for project spaces
    76  	if !c.EnableHome && strings.HasPrefix(c.Namespace, eosProjectsNamespace) {
    77  		c.AllowPathRecycleOperations = true
    78  		c.ImpersonateOwnerforRevisions = true
    79  	}
    80  
    81  	t, ok := m["mount_id_template"].(string)
    82  	if !ok || t == "" {
    83  		t = "eoshome-{{ trimAll \"/\" .Path | substr 0 1 }}"
    84  	}
    85  
    86  	return c, t, nil
    87  }
    88  
    89  // New returns an implementation of the storage.FS interface that forms a wrapper
    90  // around separate connections to EOS.
    91  func New(m map[string]interface{}, _ events.Stream, _ *zerolog.Logger) (storage.FS, error) {
    92  	c, t, err := parseConfig(m)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  
    97  	eos, err := eosfs.NewEOSFS(c)
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  
   102  	mountIDTemplate, err := template.New("mountID").Funcs(sprig.TxtFuncMap()).Parse(t)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	return &wrapper{FS: eos, conf: c, mountIDTemplate: mountIDTemplate}, nil
   108  }
   109  
   110  // We need to override the methods, GetMD, GetPathByID and ListFolder to fill the
   111  // StorageId in the ResourceInfo objects.
   112  
   113  func (w *wrapper) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []string, fieldMask []string) (*provider.ResourceInfo, error) {
   114  	res, err := w.FS.GetMD(ctx, ref, mdKeys, fieldMask)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	// We need to extract the mount ID based on the mapping template.
   120  	//
   121  	// Take the first letter of the resource path after the namespace has been removed.
   122  	// If it's empty, leave it empty to be filled by storageprovider.
   123  	res.Id.StorageId = w.getMountID(ctx, res)
   124  
   125  	if err = w.setProjectSharingPermissions(ctx, res); err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	// If the request contains a relative reference, we also need to return the base path instead of the full one
   130  	if utils.IsRelativeReference(ref) {
   131  		res.Path = path.Base(res.Path)
   132  	}
   133  
   134  	return res, nil
   135  }
   136  
   137  func (w *wrapper) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys, fieldMask []string) ([]*provider.ResourceInfo, error) {
   138  	res, err := w.FS.ListFolder(ctx, ref, mdKeys, fieldMask)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  	for _, r := range res {
   143  		r.Id.StorageId = w.getMountID(ctx, r)
   144  
   145  		// If the request contains a relative reference, we also need to return the base path instead of the full one
   146  		if utils.IsRelativeReference(ref) {
   147  			r.Path = path.Base(r.Path)
   148  		}
   149  
   150  		if err = w.setProjectSharingPermissions(ctx, r); err != nil {
   151  			continue
   152  		}
   153  	}
   154  	return res, nil
   155  }
   156  
   157  func (w *wrapper) ListRecycle(ctx context.Context, ref *provider.Reference, key, relativePath string) ([]*provider.RecycleItem, error) {
   158  	res, err := w.FS.ListRecycle(ctx, ref, key, relativePath)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	// If the request contains a relative reference, we also need to return the base path instead of the full one
   164  	if utils.IsRelativeReference(ref) {
   165  		for _, info := range res {
   166  			info.Ref.Path = path.Base(info.Ref.Path)
   167  		}
   168  	}
   169  
   170  	return res, nil
   171  
   172  }
   173  
   174  func (w *wrapper) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter, unrestricted bool) ([]*provider.StorageSpace, error) {
   175  	res, err := w.FS.ListStorageSpaces(ctx, filter, unrestricted)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	for _, r := range res {
   181  		if mountID, _, _, _ := storagespace.SplitID(r.Id.OpaqueId); mountID == "" {
   182  			mountID = w.getMountID(ctx, &provider.ResourceInfo{Path: r.Name})
   183  			r.Root.StorageId = mountID
   184  		}
   185  	}
   186  	return res, nil
   187  
   188  }
   189  
   190  func (w *wrapper) ListRevisions(ctx context.Context, ref *provider.Reference) ([]*provider.FileVersion, error) {
   191  	if err := w.userIsProjectAdmin(ctx, ref); err != nil {
   192  		return nil, err
   193  	}
   194  
   195  	return w.FS.ListRevisions(ctx, ref)
   196  }
   197  
   198  func (w *wrapper) DownloadRevision(ctx context.Context, ref *provider.Reference, revisionKey string, openReaderfunc func(*provider.ResourceInfo) bool) (*provider.ResourceInfo, io.ReadCloser, error) {
   199  	if err := w.userIsProjectAdmin(ctx, ref); err != nil {
   200  		return nil, nil, err
   201  	}
   202  
   203  	return w.FS.DownloadRevision(ctx, ref, revisionKey, openReaderfunc)
   204  }
   205  
   206  func (w *wrapper) RestoreRevision(ctx context.Context, ref *provider.Reference, revisionKey string) error {
   207  	if err := w.userIsProjectAdmin(ctx, ref); err != nil {
   208  		return err
   209  	}
   210  
   211  	return w.FS.RestoreRevision(ctx, ref, revisionKey)
   212  }
   213  
   214  func (w *wrapper) DenyGrant(ctx context.Context, ref *provider.Reference, g *provider.Grantee) error {
   215  	// This is only allowed for project space admins
   216  	if strings.HasPrefix(w.conf.Namespace, eosProjectsNamespace) {
   217  		if err := w.userIsProjectAdmin(ctx, ref); err != nil {
   218  			return err
   219  		}
   220  		return w.FS.DenyGrant(ctx, ref, g)
   221  	}
   222  
   223  	return errtypes.NotSupported("eos: deny grant is only enabled for project spaces")
   224  }
   225  
   226  func (w *wrapper) getMountID(ctx context.Context, r *provider.ResourceInfo) string {
   227  	if r == nil {
   228  		return ""
   229  	}
   230  	r.Path = strings.TrimPrefix(r.Path, w.conf.MountPath)
   231  	b := bytes.Buffer{}
   232  	if err := w.mountIDTemplate.Execute(&b, r); err != nil {
   233  		return ""
   234  	}
   235  	r.Path = path.Join(w.conf.MountPath, r.Path)
   236  	return b.String()
   237  }
   238  
   239  func (w *wrapper) setProjectSharingPermissions(ctx context.Context, r *provider.ResourceInfo) error {
   240  	// Check if this storage provider corresponds to a project spaces instance
   241  	if strings.HasPrefix(w.conf.Namespace, eosProjectsNamespace) {
   242  
   243  		// Extract project name from the path resembling /c/cernbox or /c/cernbox/minutes/..
   244  		parts := strings.SplitN(r.Path, "/", 4)
   245  		if len(parts) != 4 && len(parts) != 3 {
   246  			// The request might be for / or /$letter
   247  			// Nothing to do in that case
   248  			return nil
   249  		}
   250  		adminGroup := projectSpaceGroupsPrefix + parts[2] + projectSpaceAdminGroupsSuffix
   251  		user := ctxpkg.ContextMustGetUser(ctx)
   252  
   253  		for _, g := range user.Groups {
   254  			if g == adminGroup {
   255  				r.PermissionSet.AddGrant = true
   256  				r.PermissionSet.RemoveGrant = true
   257  				r.PermissionSet.UpdateGrant = true
   258  				r.PermissionSet.ListGrants = true
   259  				r.PermissionSet.GetQuota = true
   260  				r.PermissionSet.DenyGrant = true
   261  				return nil
   262  			}
   263  		}
   264  	}
   265  	return nil
   266  }
   267  
   268  func (w *wrapper) userIsProjectAdmin(ctx context.Context, ref *provider.Reference) error {
   269  	// Check if this storage provider corresponds to a project spaces instance
   270  	if !strings.HasPrefix(w.conf.Namespace, eosProjectsNamespace) {
   271  		return nil
   272  	}
   273  
   274  	res, err := w.FS.GetMD(ctx, ref, nil, nil)
   275  	if err != nil {
   276  		return err
   277  	}
   278  
   279  	// Extract project name from the path resembling /c/cernbox or /c/cernbox/minutes/..
   280  	parts := strings.SplitN(res.Path, "/", 4)
   281  	if len(parts) != 4 && len(parts) != 3 {
   282  		// The request might be for / or /$letter
   283  		// Nothing to do in that case
   284  		return nil
   285  	}
   286  	adminGroup := projectSpaceGroupsPrefix + parts[2] + projectSpaceAdminGroupsSuffix
   287  	user := ctxpkg.ContextMustGetUser(ctx)
   288  
   289  	for _, g := range user.Groups {
   290  		if g == adminGroup {
   291  			return nil
   292  		}
   293  	}
   294  
   295  	return errtypes.PermissionDenied("eosfs: project spaces revisions can only be accessed by admins")
   296  }