github.com/cs3org/reva/v2@v2.27.7/internal/http/services/appprovider/appprovider.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 appprovider 20 21 import ( 22 "context" 23 "encoding/json" 24 "net/http" 25 "net/url" 26 "path" 27 "strings" 28 29 appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1" 30 providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" 31 gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" 32 rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" 33 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 34 ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" 35 "github.com/cs3org/reva/v2/pkg/rgrpc/status" 36 "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" 37 "github.com/cs3org/reva/v2/pkg/rhttp/global" 38 "github.com/cs3org/reva/v2/pkg/sharedconf" 39 "github.com/cs3org/reva/v2/pkg/storagespace" 40 "github.com/cs3org/reva/v2/pkg/utils" 41 iso6391 "github.com/emvi/iso-639-1" 42 "github.com/go-chi/chi/v5" 43 ua "github.com/mileusna/useragent" 44 "github.com/mitchellh/mapstructure" 45 "github.com/pkg/errors" 46 "github.com/rs/zerolog" 47 "google.golang.org/protobuf/proto" 48 ) 49 50 func init() { 51 global.Register("appprovider", New) 52 } 53 54 // Config holds the config options for the HTTP appprovider service 55 type Config struct { 56 Prefix string `mapstructure:"prefix"` 57 GatewaySvc string `mapstructure:"gatewaysvc"` 58 Insecure bool `mapstructure:"insecure"` 59 WebBaseURI string `mapstructure:"webbaseuri"` 60 Web Web `mapstructure:"web"` 61 SecureViewAppAddr string `mapstructure:"secure_view_app_addr"` 62 } 63 64 // Web holds the config options for the URL parameters for Web 65 type Web struct { 66 URLParamsMapping map[string]string `mapstructure:"urlparamsmapping"` 67 StaticURLParams map[string]string `mapstructure:"staticurlparams"` 68 } 69 70 func (c *Config) init() { 71 if c.Prefix == "" { 72 c.Prefix = "app" 73 } 74 c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc) 75 } 76 77 type svc struct { 78 conf *Config 79 router *chi.Mux 80 } 81 82 // New returns a new ocmd object 83 func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) { 84 85 conf := &Config{} 86 if err := mapstructure.Decode(m, conf); err != nil { 87 return nil, err 88 } 89 conf.init() 90 91 r := chi.NewRouter() 92 s := &svc{ 93 conf: conf, 94 router: r, 95 } 96 97 if err := s.routerInit(); err != nil { 98 return nil, err 99 } 100 101 _ = chi.Walk(s.router, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { 102 log.Debug().Str("service", "approvider").Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint") 103 return nil 104 }) 105 106 return s, nil 107 } 108 109 const ( 110 openModeNormal = iota 111 openModeWeb 112 ) 113 114 func (s *svc) routerInit() error { 115 s.router.Get("/list", s.handleList) 116 s.router.Post("/new", s.handleNew) 117 s.router.Post("/open", s.handleOpen(openModeNormal)) 118 s.router.Post("/open-with-web", s.handleOpen(openModeWeb)) 119 return nil 120 } 121 122 // Close performs cleanup. 123 func (s *svc) Close() error { 124 return nil 125 } 126 127 func (s *svc) Prefix() string { 128 return s.conf.Prefix 129 } 130 131 func (s *svc) Unprotected() []string { 132 return []string{"/list"} 133 } 134 135 func (s *svc) Handler() http.Handler { 136 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 137 s.router.ServeHTTP(w, r) 138 }) 139 } 140 141 func (s *svc) handleNew(w http.ResponseWriter, r *http.Request) { 142 ctx := r.Context() 143 144 client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc) 145 if err != nil { 146 writeError(w, r, appErrorServerError, "error getting grpc gateway client", err) 147 return 148 } 149 150 err = r.ParseForm() 151 if err != nil { 152 writeError(w, r, appErrorInvalidParameter, "parameters could not be parsed", nil) 153 } 154 155 if r.Form.Get("template") != "" { 156 // TODO in the future we want to create a file out of the given template 157 writeError(w, r, appErrorUnimplemented, "template is not implemented", nil) 158 return 159 } 160 161 parentContainerIDStr := r.Form.Get("parent_container_id") 162 if parentContainerIDStr == "" { 163 writeError(w, r, appErrorInvalidParameter, "missing parent container ID", nil) 164 return 165 } 166 167 parentContainerID, err := storagespace.ParseID(parentContainerIDStr) 168 if err != nil { 169 writeError(w, r, appErrorInvalidParameter, "invalid parent container ID", nil) 170 return 171 } 172 173 filename := r.Form.Get("filename") 174 if filename == "" { 175 writeError(w, r, appErrorInvalidParameter, "missing filename", nil) 176 return 177 } 178 179 dirPart, filePart := path.Split(filename) 180 if dirPart != "" || filePart != filename { 181 writeError(w, r, appErrorInvalidParameter, "the filename must not contain a path segment", nil) 182 return 183 } 184 185 statParentContainerReq := &provider.StatRequest{ 186 Ref: &provider.Reference{ 187 ResourceId: &parentContainerID, 188 }, 189 } 190 parentContainer, err := client.Stat(ctx, statParentContainerReq) 191 if err != nil { 192 writeError(w, r, appErrorServerError, "error sending a grpc stat request", err) 193 return 194 } 195 196 if parentContainer.Status.Code != rpc.Code_CODE_OK { 197 writeError(w, r, appErrorNotFound, "the parent container is not accessible or does not exist", err) 198 return 199 } 200 201 if parentContainer.Info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER { 202 writeError(w, r, appErrorInvalidParameter, "the parent container id does not point to a container", nil) 203 return 204 } 205 206 fileRef := &provider.Reference{ 207 ResourceId: &parentContainerID, 208 Path: utils.MakeRelativePath(filename), 209 } 210 211 statFileReq := &provider.StatRequest{ 212 Ref: fileRef, 213 } 214 statFileRes, err := client.Stat(ctx, statFileReq) 215 if err != nil { 216 writeError(w, r, appErrorServerError, "failed to stat the file", err) 217 return 218 } 219 220 if statFileRes.Status.Code != rpc.Code_CODE_NOT_FOUND { 221 if statFileRes.Status.Code == rpc.Code_CODE_OK { 222 writeError(w, r, appErrorAlreadyExists, "the file already exists", nil) 223 return 224 } 225 writeError(w, r, appErrorServerError, "statting the file returned unexpected status code", err) 226 return 227 } 228 229 touchFileReq := &provider.TouchFileRequest{ 230 Ref: fileRef, 231 } 232 233 touchRes, err := client.TouchFile(ctx, touchFileReq) 234 if err != nil { 235 writeError(w, r, appErrorServerError, "error sending a grpc touchfile request", err) 236 return 237 } 238 239 if touchRes.Status.Code != rpc.Code_CODE_OK { 240 if touchRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { 241 writeError(w, r, appErrorPermissionDenied, "permission denied to create the file", nil) 242 return 243 } 244 writeError(w, r, appErrorServerError, "touching the file failed", nil) 245 return 246 } 247 248 // Stat the newly created file 249 statRes, err := client.Stat(ctx, statFileReq) 250 if err != nil { 251 writeError(w, r, appErrorServerError, "statting the created file failed", err) 252 return 253 } 254 255 if statRes.Status.Code != rpc.Code_CODE_OK { 256 writeError(w, r, appErrorServerError, "statting the created file failed", nil) 257 return 258 } 259 260 if statRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_FILE { 261 writeError(w, r, appErrorInvalidParameter, "the given file id does not point to a file", nil) 262 return 263 } 264 fileid := storagespace.FormatResourceID(statRes.Info.Id) 265 266 js, err := json.Marshal( 267 map[string]interface{}{ 268 "file_id": fileid, 269 }, 270 ) 271 if err != nil { 272 writeError(w, r, appErrorServerError, "error marshalling JSON response", err) 273 return 274 } 275 276 w.Header().Set("Content-Type", "application/json") 277 if _, err = w.Write(js); err != nil { 278 writeError(w, r, appErrorServerError, "error writing JSON response", err) 279 return 280 } 281 } 282 283 func (s *svc) handleList(w http.ResponseWriter, r *http.Request) { 284 ctx := r.Context() 285 client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc) 286 if err != nil { 287 writeError(w, r, appErrorServerError, "error getting grpc gateway client", err) 288 return 289 } 290 291 listRes, err := client.ListSupportedMimeTypes(ctx, &appregistry.ListSupportedMimeTypesRequest{}) 292 if err != nil { 293 writeError(w, r, appErrorServerError, "error listing supported mime types", err) 294 return 295 } 296 if listRes.Status.Code != rpc.Code_CODE_OK { 297 writeError(w, r, appErrorServerError, "error listing supported mime types", nil) 298 return 299 } 300 301 res := buildApps(listRes.MimeTypes, r.UserAgent(), s.conf.SecureViewAppAddr) 302 303 js, err := json.Marshal(map[string]interface{}{"mime-types": res}) 304 if err != nil { 305 writeError(w, r, appErrorServerError, "error marshalling JSON response", err) 306 return 307 } 308 309 w.Header().Set("Content-Type", "application/json") 310 if _, err = w.Write(js); err != nil { 311 writeError(w, r, appErrorServerError, "error writing JSON response", err) 312 return 313 } 314 } 315 316 func (s *svc) handleOpen(openMode int) http.HandlerFunc { 317 318 return func(w http.ResponseWriter, r *http.Request) { 319 ctx := r.Context() 320 321 client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc) 322 if err != nil { 323 writeError(w, r, appErrorServerError, "Internal error with the gateway, please try again later", err) 324 return 325 } 326 327 err = r.ParseForm() 328 if err != nil { 329 writeError(w, r, appErrorInvalidParameter, "parameters could not be parsed", nil) 330 } 331 332 lang := r.Form.Get("lang") 333 parts := strings.Split(lang, "_") 334 if lang != "" && !iso6391.ValidCode(parts[0]) { 335 writeError(w, r, appErrorInvalidParameter, "lang parameter does not contain a valid ISO 639-1 language code in the language tag", nil) 336 return 337 } 338 339 fileID := r.Form.Get("file_id") 340 341 if fileID == "" { 342 writeError(w, r, appErrorInvalidParameter, "missing file ID", nil) 343 return 344 } 345 346 resourceID, err := storagespace.ParseID(fileID) 347 if err != nil { 348 writeError(w, r, appErrorInvalidParameter, "invalid file ID", nil) 349 return 350 } 351 352 fileRef := &provider.Reference{ 353 ResourceId: &resourceID, 354 Path: ".", 355 } 356 357 statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: fileRef}) 358 if err != nil { 359 writeError(w, r, appErrorServerError, "Internal error accessing the file, please try again later", err) 360 return 361 } 362 363 if status := utils.ReadPlainFromOpaque(statRes.GetInfo().GetOpaque(), "status"); status == "processing" { 364 writeError(w, r, appErrorTooEarly, "The requested file is not yet available, please try again later", nil) 365 return 366 } 367 368 viewMode, err := getViewModeFromPublicScope(ctx) 369 if err != nil { 370 writeError(w, r, appErrorPermissionDenied, "permission denied to open the application", err) 371 return 372 } 373 374 if viewMode == gateway.OpenInAppRequest_VIEW_MODE_INVALID { 375 // we have no publicshare Role in the token scope 376 // do a stat request to assemble the permissions for this user 377 statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: fileRef}) 378 if err != nil { 379 writeError(w, r, appErrorServerError, "Internal error accessing the file, please try again later", err) 380 return 381 } 382 383 if statRes.Status.Code == rpc.Code_CODE_NOT_FOUND { 384 writeError(w, r, appErrorNotFound, "file does not exist", nil) 385 return 386 } else if statRes.Status.Code != rpc.Code_CODE_OK { 387 writeError(w, r, appErrorServerError, "failed to stat the file", nil) 388 return 389 } 390 391 if statRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_FILE { 392 writeError(w, r, appErrorInvalidParameter, "the given file id does not point to a file", nil) 393 return 394 } 395 396 // Calculate the view mode from the resource permissions 397 viewMode = getViewMode(statRes.Info, r.Form.Get("view_mode")) 398 if viewMode == gateway.OpenInAppRequest_VIEW_MODE_INVALID { 399 writeError(w, r, appErrorInvalidParameter, "invalid view mode", err) 400 return 401 } 402 } 403 404 openReq := gateway.OpenInAppRequest{ 405 Ref: fileRef, 406 ViewMode: viewMode, 407 App: r.Form.Get("app_name"), 408 Opaque: utils.AppendPlainToOpaque(nil, "lang", lang), 409 } 410 411 templateID := r.Form.Get("template_id") 412 if templateID != "" { 413 openReq.Opaque = utils.AppendPlainToOpaque(openReq.Opaque, "template", templateID) 414 } 415 openRes, err := client.OpenInApp(ctx, &openReq) 416 if err != nil { 417 writeError(w, r, appErrorServerError, 418 "Error contacting the requested application, please use a different one or try again later", err) 419 return 420 } 421 if openRes.Status.Code != rpc.Code_CODE_OK { 422 if openRes.Status.Code == rpc.Code_CODE_NOT_FOUND { 423 writeError(w, r, appErrorNotFound, openRes.Status.Message, nil) 424 return 425 } 426 writeError(w, r, appErrorServerError, openRes.Status.Message, 427 status.NewErrorFromCode(openRes.Status.Code, "error calling OpenInApp")) 428 return 429 } 430 431 var payload interface{} 432 433 switch openMode { 434 case openModeNormal: 435 payload = openRes.AppUrl 436 437 case openModeWeb: 438 payload, err = newOpenInWebResponse(s.conf.WebBaseURI, s.conf.Web.URLParamsMapping, s.conf.Web.StaticURLParams, fileID, r.Form.Get("app_name"), r.Form.Get("view_mode")) 439 if err != nil { 440 writeError(w, r, appErrorServerError, "Internal error", 441 errors.Wrap(err, "error building OpenInWeb response")) 442 return 443 } 444 445 default: 446 writeError(w, r, appErrorServerError, "Internal error with the open mode", 447 errors.New("unknown open mode")) 448 return 449 450 } 451 452 js, err := json.Marshal(payload) 453 if err != nil { 454 writeError(w, r, appErrorServerError, "Internal error with JSON payload", 455 errors.Wrap(err, "error marshalling JSON response")) 456 return 457 } 458 459 w.Header().Set("Content-Type", "application/json") 460 if _, err = w.Write(js); err != nil { 461 writeError(w, r, appErrorServerError, "Internal error with JSON payload", 462 errors.Wrap(err, "error writing JSON response")) 463 return 464 } 465 } 466 } 467 468 type openInWebResponse struct { 469 URI string `json:"uri"` 470 } 471 472 func newOpenInWebResponse(baseURI string, params, staticParams map[string]string, fileID, appName, viewMode string) (openInWebResponse, error) { 473 474 uri, err := url.Parse(baseURI) 475 if err != nil { 476 return openInWebResponse{}, err 477 } 478 479 query := uri.Query() 480 481 for key, val := range params { 482 483 switch val { 484 case "fileid": 485 if fileID != "" { 486 query.Add(key, fileID) 487 } 488 case "appname": 489 if appName != "" { 490 query.Add(key, appName) 491 } 492 case "viewmode": 493 if viewMode != "" { 494 query.Add(key, viewMode) 495 } 496 default: 497 return openInWebResponse{}, errors.New("unknown parameter mapper") 498 } 499 500 } 501 502 for key, val := range staticParams { 503 query.Add(key, val) 504 } 505 506 uri.RawQuery = query.Encode() 507 508 return openInWebResponse{URI: uri.String()}, nil 509 } 510 511 // MimeTypeInfo wraps the appregistry.MimeTypeInfo to change the app providers to ProviderInfos with a secure view flag 512 type MimeTypeInfo struct { 513 appregistry.MimeTypeInfo 514 AppProviders []*ProviderInfo `json:"app_providers"` 515 } 516 517 // ProviderInfo wraps the appregistry.ProviderInfo to add a secure view flag 518 type ProviderInfo struct { 519 appregistry.ProviderInfo 520 // TODO make this part of the CS3 provider info 521 SecureView bool `json:"secure_view"` 522 TargetExt string `json:"target_ext,omitempty"` 523 } 524 525 // buildApps rewrites the mime type info to only include apps that 526 // * have a name 527 // * can be called by the user agent, eg Desktop-only 528 // 529 // it also 530 // * wraps the provider info to be able to add a secure view flag 531 // * adds a secure view flag if the address matches the secure view app address and 532 // * removes the address from the provider info to not expose internal addresses 533 func buildApps(mimeTypes []*appregistry.MimeTypeInfo, userAgent, secureViewAppAddr string) []*MimeTypeInfo { 534 ua := ua.Parse(userAgent) 535 res := []*MimeTypeInfo{} 536 for _, m := range mimeTypes { 537 apps := []*ProviderInfo{} 538 for _, p := range m.AppProviders { 539 ep := &ProviderInfo{} 540 proto.Merge(&ep.ProviderInfo, p) 541 if p.Address == secureViewAppAddr { 542 ep.SecureView = true 543 } 544 p.Address = "" // address is internal only and not needed in the client 545 // apps are called by name, so if it has no name it cannot be called and should not be advertised 546 // also filter Desktop-only apps if ua is not Desktop 547 if p.Name != "" && (ua.Desktop || !p.DesktopOnly) { 548 apps = append(apps, ep) 549 } 550 } 551 if len(apps) > 0 { 552 mt := &MimeTypeInfo{} 553 addTemplateInfo(m, apps) 554 proto.Merge(&mt.MimeTypeInfo, m) 555 mt.AppProviders = apps 556 res = append(res, mt) 557 } 558 } 559 return res 560 } 561 562 func getViewMode(res *provider.ResourceInfo, vm string) gateway.OpenInAppRequest_ViewMode { 563 if vm != "" { 564 return utils.GetViewMode(vm) 565 } 566 567 var viewMode gateway.OpenInAppRequest_ViewMode 568 canEdit := res.PermissionSet.InitiateFileUpload 569 canView := res.PermissionSet.InitiateFileDownload 570 571 switch { 572 case canEdit && canView: 573 viewMode = gateway.OpenInAppRequest_VIEW_MODE_READ_WRITE 574 case canView: 575 viewMode = gateway.OpenInAppRequest_VIEW_MODE_READ_ONLY 576 default: 577 viewMode = gateway.OpenInAppRequest_VIEW_MODE_INVALID 578 } 579 return viewMode 580 } 581 582 // try to get the view mode from a publicshare scope 583 func getViewModeFromPublicScope(ctx context.Context) (gateway.OpenInAppRequest_ViewMode, error) { 584 scopes, ok := ctxpkg.ContextGetScopes(ctx) 585 if ok { 586 for key, scope := range scopes { 587 if strings.HasPrefix(key, "publicshare:") { 588 switch scope.GetRole() { 589 case providerv1beta1.Role_ROLE_VIEWER: 590 return gateway.OpenInAppRequest_VIEW_MODE_VIEW_ONLY, nil 591 case providerv1beta1.Role_ROLE_EDITOR: 592 return gateway.OpenInAppRequest_VIEW_MODE_READ_WRITE, nil 593 default: 594 return gateway.OpenInAppRequest_VIEW_MODE_INVALID, errors.New("invalid view mode in publicshare scope") 595 } 596 } 597 } 598 } 599 return gateway.OpenInAppRequest_VIEW_MODE_INVALID, nil 600 }