Skip to content

Commit 5807eea

Browse files
committed
feat(graph): expose @microsoft.graph.downloadUrl and /content on driveItems
Populates @microsoft.graph.downloadUrl on file driveItems when requested via $select and implements GET .../items/{item-id}/content as a 302 to the same URL: a by-id WebDAV URL signed with OC_URL_SIGNING_SECRET, verified by the proxy, valid for 30 minutes. Folders answer 404 on /content and never carry the annotation. The annotation is available on the driveItem stat, the children and root children listings and the share jail item endpoint.
1 parent 0459149 commit 5807eea

10 files changed

Lines changed: 556 additions & 2 deletions

services/graph/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ To specialize `graph` service instances in order to scale them independently, it
191191
* `GRAPH_HTTP_DISABLE`: when set to `true`, the service does not listen on HTTP and only consumes events (defaults to `false`)
192192
* `GRAPH_EVENTS_DISABLE_CONSUMER`: when set to `true`, the service does not consome events and only listens on HTTP (defaults to `false`)
193193

194+
## Download URLs
195+
196+
`GET /drives/{drive-id}/items/{item-id}/content` and the `@microsoft.graph.downloadUrl` annotation (requested via `$select`) hand out WebDAV URLs signed with `OC_URL_SIGNING_SECRET`. The proxy verifies the signature, so the URLs work without an `Authorization` header. They expire after 30 minutes. Without the secret the annotation is omitted and the `content` endpoint answers with an error.
197+
194198
## Metrics
195199

196200
Metrics are disabled by default, and must be enabled using the following environment variables:

services/graph/mocks/base_graph_provider.go

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/graph/pkg/service/v0/api_drives_drive_item.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,9 @@ func (api DrivesDriveItemApi) GetDriveItem(w http.ResponseWriter, r *http.Reques
416416
ErrDriveItemConversion.Render(w, r)
417417
return
418418
}
419+
if driveItemPropertySelected(r, _selectDownloadURL) {
420+
api.baseGraphService.SetDriveItemsDownloadURL(r, driveItems)
421+
}
419422

420423
render.Status(r, http.StatusOK)
421424
render.JSON(w, r, driveItems[0])

services/graph/pkg/service/v0/api_drives_drive_item_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,27 @@ var _ = Describe("DrivesDriveItemApi", func() {
980980
jsonData := gjson.Get(w.Body.String(), "error")
981981
Expect(jsonData.Get("code").String() + ": " + jsonData.Get("message").String()).To(Equal(svc.ErrDriveItemConversion.Error()))
982982
})
983+
984+
It("adds the download url when selected via $select", func() {
985+
baseGraphProvider.
986+
EXPECT().
987+
CS3ReceivedSharesToDriveItems(mock.Anything, mock.Anything).
988+
Return([]libregraph.DriveItem{{}}, nil).
989+
Once()
990+
baseGraphProvider.
991+
EXPECT().
992+
SetDriveItemsDownloadURL(mock.Anything, mock.Anything).
993+
Return().
994+
Once()
995+
996+
r = httptest.NewRequest(http.MethodGet, "/?$select=@microsoft.graph.downloadUrl", nil).
997+
WithContext(
998+
context.WithValue(context.Background(), chi.RouteCtxKey, rCTX),
999+
)
1000+
1001+
drivesDriveItemApi.GetDriveItem(w, r)
1002+
Expect(w.Code).To(Equal(http.StatusOK))
1003+
})
9831004
})
9841005

9851006
It("successfully returns the share", func() {

services/graph/pkg/service/v0/base.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
"net/http"
89
"net/url"
910
"path"
1011
"time"
@@ -23,6 +24,7 @@ import (
2324

2425
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
2526
"github.com/opencloud-eu/reva/v2/pkg/share"
27+
"github.com/opencloud-eu/reva/v2/pkg/signedurl"
2628
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
2729
"github.com/opencloud-eu/reva/v2/pkg/utils"
2830

@@ -39,6 +41,7 @@ import (
3941
type BaseGraphProvider interface {
4042
CS3ReceivedSharesToDriveItems(ctx context.Context, receivedShares []*collaboration.ReceivedShare) ([]libregraph.DriveItem, error)
4143
CS3ReceivedOCMSharesToDriveItems(ctx context.Context, receivedOCMShares []*ocm.ReceivedShare) ([]libregraph.DriveItem, error)
44+
SetDriveItemsDownloadURL(r *http.Request, items []libregraph.DriveItem)
4245
}
4346

4447
// BaseGraphService implements a couple of helper functions that are
@@ -50,6 +53,7 @@ type BaseGraphService struct {
5053
config *config.Config
5154
availableRoles []*libregraph.UnifiedRoleDefinition
5255
publicBaseURL *url.URL
56+
downloadSigner signedurl.Signer
5357
}
5458

5559
// webURLForResource returns the public web URL pointing at the given resource
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package svc
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"path"
7+
"time"
8+
9+
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
10+
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
11+
libregraph "github.com/opencloud-eu/libre-graph-api-go"
12+
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
13+
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
14+
15+
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
16+
)
17+
18+
const downloadURLTTL = 30 * time.Minute
19+
20+
// ErrDownloadURLSigningNotConfigured is returned when no url signing secret is configured
21+
var ErrDownloadURLSigningNotConfigured = errors.New("download url signing is not configured")
22+
23+
// GetDriveItemContent redirects to a signed download url for a file
24+
func (g Graph) GetDriveItemContent(w http.ResponseWriter, r *http.Request) {
25+
ctx := r.Context()
26+
27+
driveID, err := parseIDParam(r, "driveID")
28+
if err != nil {
29+
errorcode.RenderError(w, r, err)
30+
return
31+
}
32+
itemID, err := parseIDParam(r, "itemID")
33+
if err != nil {
34+
errorcode.RenderError(w, r, err)
35+
return
36+
}
37+
if driveID.GetStorageId() != itemID.GetStorageId() || driveID.GetSpaceId() != itemID.GetSpaceId() {
38+
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Item does not exist")
39+
return
40+
}
41+
42+
user, ok := revactx.ContextGetUser(ctx)
43+
if !ok {
44+
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "user not in context")
45+
return
46+
}
47+
48+
gatewayClient, err := g.gatewaySelector.Next()
49+
if err != nil {
50+
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
51+
return
52+
}
53+
stat, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: &itemID}})
54+
switch {
55+
case err != nil:
56+
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
57+
return
58+
case stat.GetStatus().GetCode() == cs3rpc.Code_CODE_OK:
59+
case stat.GetStatus().GetCode() == cs3rpc.Code_CODE_NOT_FOUND:
60+
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, stat.GetStatus().GetMessage())
61+
return
62+
case stat.GetStatus().GetCode() == cs3rpc.Code_CODE_PERMISSION_DENIED:
63+
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, stat.GetStatus().GetMessage())
64+
return
65+
case stat.GetStatus().GetCode() == cs3rpc.Code_CODE_UNAUTHENTICATED:
66+
errorcode.Unauthenticated.Render(w, r, http.StatusUnauthorized, stat.GetStatus().GetMessage())
67+
return
68+
default:
69+
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, stat.GetStatus().GetMessage())
70+
return
71+
}
72+
if stat.GetInfo().GetType() != storageprovider.ResourceType_RESOURCE_TYPE_FILE {
73+
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Item is not a file")
74+
return
75+
}
76+
77+
downloadURL, err := g.signedDownloadURL(&itemID, user.GetId().GetOpaqueId())
78+
if err != nil {
79+
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
80+
return
81+
}
82+
83+
http.Redirect(w, r, downloadURL, http.StatusFound)
84+
}
85+
86+
// SetDriveItemsDownloadURL adds a signed download url to every file in items when requested via $select
87+
func (g BaseGraphService) SetDriveItemsDownloadURL(r *http.Request, items []libregraph.DriveItem) {
88+
if !g.downloadURLRequested(r) {
89+
return
90+
}
91+
user, ok := revactx.ContextGetUser(r.Context())
92+
if !ok {
93+
return
94+
}
95+
for i := range items {
96+
g.signDriveItemDownloadURL(&items[i], user.GetId().GetOpaqueId())
97+
}
98+
}
99+
100+
func (g BaseGraphService) setDriveItemDownloadURL(r *http.Request, item *libregraph.DriveItem) {
101+
if !g.downloadURLRequested(r) {
102+
return
103+
}
104+
user, ok := revactx.ContextGetUser(r.Context())
105+
if !ok {
106+
return
107+
}
108+
g.signDriveItemDownloadURL(item, user.GetId().GetOpaqueId())
109+
}
110+
111+
func (g BaseGraphService) signDriveItemDownloadURL(item *libregraph.DriveItem, userID string) {
112+
if item.File == nil {
113+
return
114+
}
115+
id, err := storagespace.ParseID(item.GetId())
116+
if err != nil {
117+
g.logger.Debug().Err(err).Str("id", item.GetId()).Msg("could not parse drive item id for the download url")
118+
return
119+
}
120+
u, err := g.signedDownloadURL(&id, userID)
121+
if err != nil {
122+
g.logger.Debug().Err(err).Str("id", item.GetId()).Msg("could not sign the download url")
123+
return
124+
}
125+
item.MicrosoftGraphDownloadUrl = &u
126+
}
127+
128+
func (g BaseGraphService) signedDownloadURL(id *storageprovider.ResourceId, userID string) (string, error) {
129+
if g.downloadSigner == nil {
130+
return "", ErrDownloadURLSigningNotConfigured
131+
}
132+
base, err := g.getWebDavBaseURL()
133+
if err != nil {
134+
return "", err
135+
}
136+
base.Path = path.Join(base.Path, storagespace.FormatResourceID(id))
137+
return g.downloadSigner.Sign(base.String(), userID, downloadURLTTL)
138+
}
139+
140+
func (g BaseGraphService) downloadURLRequested(r *http.Request) bool {
141+
return g.downloadSigner != nil && driveItemPropertySelected(r, _selectDownloadURL)
142+
}

0 commit comments

Comments
 (0)