github.com/cs3org/reva/v2@v2.27.7/internal/http/services/owncloud/ocdav/ocdav.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 ocdav 20 21 import ( 22 "context" 23 "net/http" 24 "path" 25 "strings" 26 "time" 27 28 gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" 29 userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" 30 rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" 31 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 32 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/config" 33 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net" 34 "github.com/cs3org/reva/v2/pkg/appctx" 35 ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" 36 "github.com/cs3org/reva/v2/pkg/errtypes" 37 "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" 38 "github.com/cs3org/reva/v2/pkg/rhttp" 39 "github.com/cs3org/reva/v2/pkg/rhttp/global" 40 "github.com/cs3org/reva/v2/pkg/rhttp/router" 41 "github.com/cs3org/reva/v2/pkg/storage/favorite" 42 "github.com/cs3org/reva/v2/pkg/storage/favorite/registry" 43 "github.com/cs3org/reva/v2/pkg/storage/utils/templates" 44 "github.com/cs3org/reva/v2/pkg/utils" 45 "github.com/jellydator/ttlcache/v2" 46 "github.com/mitchellh/mapstructure" 47 "github.com/rs/zerolog" 48 "google.golang.org/grpc/codes" 49 "google.golang.org/grpc/metadata" 50 "google.golang.org/grpc/status" 51 ) 52 53 // name is the Tracer name used to identify this instrumentation library. 54 const tracerName = "ocdav" 55 56 func init() { 57 global.Register("ocdav", New) 58 } 59 60 type svc struct { 61 c *config.Config 62 webDavHandler *WebDavHandler 63 davHandler *DavHandler 64 favoritesManager favorite.Manager 65 client *http.Client 66 gatewaySelector pool.Selectable[gateway.GatewayAPIClient] 67 // LockSystem is the lock management system. 68 LockSystem LockSystem 69 userIdentifierCache *ttlcache.Cache 70 nameValidators []Validator 71 } 72 73 func (s *svc) Config() *config.Config { 74 return s.c 75 } 76 77 func getFavoritesManager(c *config.Config) (favorite.Manager, error) { 78 if f, ok := registry.NewFuncs[c.FavoriteStorageDriver]; ok { 79 return f(c.FavoriteStorageDrivers[c.FavoriteStorageDriver]) 80 } 81 return nil, errtypes.NotFound("driver not found: " + c.FavoriteStorageDriver) 82 } 83 func getLockSystem(c *config.Config) (LockSystem, error) { 84 // TODO in memory implementation 85 selector, err := pool.GatewaySelector(c.GatewaySvc) 86 if err != nil { 87 return nil, err 88 } 89 return NewCS3LS(selector), nil 90 } 91 92 // New returns a new ocdav service 93 func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) { 94 conf := &config.Config{} 95 if err := mapstructure.Decode(m, conf); err != nil { 96 return nil, err 97 } 98 99 conf.Init() 100 101 fm, err := getFavoritesManager(conf) 102 if err != nil { 103 return nil, err 104 } 105 ls, err := getLockSystem(conf) 106 if err != nil { 107 return nil, err 108 } 109 110 return NewWith(conf, fm, ls, log, nil) 111 } 112 113 // NewWith returns a new ocdav service 114 func NewWith(conf *config.Config, fm favorite.Manager, ls LockSystem, _ *zerolog.Logger, selector pool.Selectable[gateway.GatewayAPIClient]) (global.Service, error) { 115 // be safe - init the conf again 116 conf.Init() 117 118 s := &svc{ 119 c: conf, 120 webDavHandler: new(WebDavHandler), 121 davHandler: new(DavHandler), 122 client: rhttp.GetHTTPClient( 123 rhttp.Timeout(time.Duration(conf.Timeout*int64(time.Second))), 124 rhttp.Insecure(conf.Insecure), 125 ), 126 gatewaySelector: selector, 127 favoritesManager: fm, 128 LockSystem: ls, 129 userIdentifierCache: ttlcache.NewCache(), 130 nameValidators: ValidatorsFromConfig(conf), 131 } 132 _ = s.userIdentifierCache.SetTTL(60 * time.Second) 133 134 // initialize handlers and set default configs 135 if err := s.webDavHandler.init(conf.WebdavNamespace, true); err != nil { 136 return nil, err 137 } 138 if err := s.davHandler.init(conf); err != nil { 139 return nil, err 140 } 141 if selector == nil { 142 var err error 143 s.gatewaySelector, err = pool.GatewaySelector(s.c.GatewaySvc) 144 if err != nil { 145 return nil, err 146 } 147 } 148 return s, nil 149 } 150 151 func (s *svc) Prefix() string { 152 return s.c.Prefix 153 } 154 155 func (s *svc) Close() error { 156 return nil 157 } 158 159 func (s *svc) Unprotected() []string { 160 return []string{"/status.php", "/status", "/remote.php/dav/public-files/", "/apps/files/", "/index.php/f/", "/index.php/s/", "/remote.php/dav/ocm/", "/dav/ocm/"} 161 } 162 163 func (s *svc) Handler() http.Handler { 164 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 165 ctx := r.Context() 166 log := appctx.GetLogger(ctx) 167 168 // TODO(jfd): do we need this? 169 // fake litmus testing for empty namespace: see https://github.com/golang/net/blob/e514e69ffb8bc3c76a71ae40de0118d794855992/webdav/litmus_test_server.go#L58-L89 170 if r.Header.Get(net.HeaderLitmus) == "props: 3 (propfind_invalid2)" { 171 http.Error(w, "400 Bad Request", http.StatusBadRequest) 172 return 173 } 174 175 // to build correct href prop urls we need to keep track of the base path 176 // always starts with / 177 base := path.Join("/", s.Prefix()) 178 179 var head string 180 head, r.URL.Path = router.ShiftPath(r.URL.Path) 181 log.Debug().Str("method", r.Method).Str("head", head).Str("tail", r.URL.Path).Msg("http routing") 182 switch head { 183 case "status.php", "status": 184 s.doStatus(w, r) 185 return 186 case "remote.php": 187 // skip optional "remote.php" 188 head, r.URL.Path = router.ShiftPath(r.URL.Path) 189 190 // yet, add it to baseURI 191 base = path.Join(base, "remote.php") 192 case "apps": 193 head, r.URL.Path = router.ShiftPath(r.URL.Path) 194 if head == "files" { 195 s.handleLegacyPath(w, r) 196 return 197 } 198 case "index.php": 199 head, r.URL.Path = router.ShiftPath(r.URL.Path) 200 if head == "s" { 201 token := r.URL.Path 202 rURL := s.c.PublicURL + path.Join(head, token) 203 204 http.Redirect(w, r, rURL, http.StatusMovedPermanently) 205 return 206 } 207 } 208 switch head { 209 // the old `/webdav` endpoint uses remote.php/webdav/$path 210 case "webdav": 211 // for oc we need to prepend /home as the path that will be passed to the home storage provider 212 // will not contain the username 213 base = path.Join(base, "webdav") 214 ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base) 215 r = r.WithContext(ctx) 216 s.webDavHandler.Handler(s).ServeHTTP(w, r) 217 return 218 case "dav": 219 // cern uses /dav/files/$namespace -> /$namespace/... 220 // oc uses /dav/files/$user -> /$home/$user/... 221 // for oc we need to prepend the path to user homes 222 // or we take the path starting at /dav and allow rewriting it? 223 base = path.Join(base, "dav") 224 ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base) 225 r = r.WithContext(ctx) 226 s.davHandler.Handler(s).ServeHTTP(w, r) 227 return 228 } 229 log.Warn().Msg("resource not found") 230 w.WriteHeader(http.StatusNotFound) 231 }) 232 } 233 234 func (s *svc) ApplyLayout(ctx context.Context, ns string, useLoggedInUserNS bool, requestPath string) (string, string, error) { 235 // If useLoggedInUserNS is false, that implies that the request is coming from 236 // the FilesHandler method invoked by a /dav/files/fileOwner where fileOwner 237 // is not the same as the logged in user. In that case, we'll treat fileOwner 238 // as the username whose files are to be accessed and use that in the 239 // namespace template. 240 u, ok := ctxpkg.ContextGetUser(ctx) 241 if !ok || !useLoggedInUserNS { 242 var requestUsernameOrID string 243 requestUsernameOrID, requestPath = router.ShiftPath(requestPath) 244 245 // Check if this is a Userid 246 client, err := s.gatewaySelector.Next() 247 if err != nil { 248 return "", "", err 249 } 250 251 userRes, err := client.GetUser(ctx, &userpb.GetUserRequest{ 252 UserId: &userpb.UserId{OpaqueId: requestUsernameOrID}, 253 }) 254 if err != nil { 255 return "", "", err 256 } 257 258 // If it's not a userid try if it is a user name 259 if userRes.Status.Code != rpc.Code_CODE_OK { 260 res, err := client.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{ 261 Claim: "username", 262 Value: requestUsernameOrID, 263 }) 264 if err != nil { 265 return "", "", err 266 } 267 userRes.Status = res.Status 268 userRes.User = res.User 269 } 270 271 // If still didn't find a user, fallback 272 if userRes.Status.Code != rpc.Code_CODE_OK { 273 userRes.User = &userpb.User{ 274 Username: requestUsernameOrID, 275 Id: &userpb.UserId{OpaqueId: requestUsernameOrID}, 276 } 277 } 278 279 u = userRes.User 280 } 281 282 return templates.WithUser(u, ns), requestPath, nil 283 } 284 285 func authContextForUser(client gateway.GatewayAPIClient, userID *userpb.UserId, machineAuthAPIKey string) (context.Context, error) { 286 if machineAuthAPIKey == "" { 287 return nil, errtypes.NotSupported("machine auth not configured") 288 } 289 // Get auth 290 granteeCtx := ctxpkg.ContextSetUser(context.Background(), &userpb.User{Id: userID}) 291 292 authRes, err := client.Authenticate(granteeCtx, &gateway.AuthenticateRequest{ 293 Type: "machine", 294 ClientId: "userid:" + userID.OpaqueId, 295 ClientSecret: machineAuthAPIKey, 296 }) 297 if err != nil { 298 return nil, err 299 } 300 if authRes.GetStatus().GetCode() != rpc.Code_CODE_OK { 301 return nil, errtypes.NewErrtypeFromStatus(authRes.Status) 302 } 303 granteeCtx = metadata.AppendToOutgoingContext(granteeCtx, ctxpkg.TokenHeader, authRes.Token) 304 return granteeCtx, nil 305 } 306 307 func (s *svc) sspReferenceIsChildOf(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], child, parent *provider.Reference) (bool, error) { 308 client, err := selector.Next() 309 if err != nil { 310 return false, err 311 } 312 parentStatRes, err := client.Stat(ctx, &provider.StatRequest{Ref: parent}) 313 if err != nil { 314 return false, err 315 } 316 if parentStatRes.GetStatus().GetCode() != rpc.Code_CODE_OK { 317 return false, errtypes.NewErrtypeFromStatus(parentStatRes.GetStatus()) 318 } 319 parentAuthCtx, err := authContextForUser(client, parentStatRes.GetInfo().GetOwner(), s.c.MachineAuthAPIKey) 320 if err != nil { 321 return false, err 322 } 323 parentPathRes, err := client.GetPath(parentAuthCtx, &provider.GetPathRequest{ResourceId: parentStatRes.GetInfo().GetId()}) 324 if err != nil { 325 return false, err 326 } 327 328 childStatRes, err := client.Stat(ctx, &provider.StatRequest{Ref: child}) 329 if err != nil { 330 return false, err 331 } 332 if childStatRes.GetStatus().GetCode() == rpc.Code_CODE_NOT_FOUND && utils.IsRelativeReference(child) && child.Path != "." { 333 childParentRef := &provider.Reference{ 334 ResourceId: child.ResourceId, 335 Path: utils.MakeRelativePath(path.Dir(child.Path)), 336 } 337 childStatRes, err = client.Stat(ctx, &provider.StatRequest{Ref: childParentRef}) 338 if err != nil { 339 return false, err 340 } 341 } 342 if childStatRes.GetStatus().GetCode() != rpc.Code_CODE_OK { 343 return false, errtypes.NewErrtypeFromStatus(parentStatRes.Status) 344 } 345 // TODO: this should use service accounts https://github.com/owncloud/ocis/issues/7597 346 childAuthCtx, err := authContextForUser(client, childStatRes.GetInfo().GetOwner(), s.c.MachineAuthAPIKey) 347 if err != nil { 348 return false, err 349 } 350 childPathRes, err := client.GetPath(childAuthCtx, &provider.GetPathRequest{ResourceId: childStatRes.GetInfo().GetId()}) 351 if err != nil { 352 return false, err 353 } 354 355 cp := childPathRes.Path + "/" 356 pp := parentPathRes.Path + "/" 357 return strings.HasPrefix(cp, pp), nil 358 } 359 360 func (s *svc) referenceIsChildOf(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], child, parent *provider.Reference) (bool, error) { 361 if child.ResourceId.SpaceId != parent.ResourceId.SpaceId { 362 return false, nil // Not on the same storage -> not a child 363 } 364 365 if utils.ResourceIDEqual(child.ResourceId, parent.ResourceId) { 366 return strings.HasPrefix(child.Path, parent.Path+"/"), nil // Relative to the same resource -> compare paths 367 } 368 369 if child.ResourceId.SpaceId == utils.ShareStorageSpaceID || parent.ResourceId.SpaceId == utils.ShareStorageSpaceID { 370 // the sharesstorageprovider needs some special handling 371 return s.sspReferenceIsChildOf(ctx, selector, child, parent) 372 } 373 374 client, err := selector.Next() 375 if err != nil { 376 return false, err 377 } 378 379 // the references are on the same storage but relative to different resources 380 // -> we need to get the path for both resources 381 childPathRes, err := client.GetPath(ctx, &provider.GetPathRequest{ResourceId: child.ResourceId}) 382 if err != nil { 383 if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented { 384 return false, nil // the storage provider doesn't support GetPath() -> rely on it taking care of recursion issues 385 } 386 return false, err 387 } 388 parentPathRes, err := client.GetPath(ctx, &provider.GetPathRequest{ResourceId: parent.ResourceId}) 389 if err != nil { 390 return false, err 391 } 392 393 cp := path.Join(childPathRes.Path, child.Path) + "/" 394 pp := path.Join(parentPathRes.Path, parent.Path) + "/" 395 return strings.HasPrefix(cp, pp), nil 396 } 397 398 // filename returns the base filename from a path and replaces any slashes with an empty string 399 func filename(p string) string { 400 return strings.Trim(path.Base(p), "/") 401 }