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 }