mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-12 13:49:13 -04:00
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.
This commit is contained in:
1 parent
0459149e0c
commit
5807eea186
10 files changed
+556
-2
No files matched your search
@@ -191,6 +191,10 @@ To specialize `graph` service instances in order to scale them independently, it
|
||||
* `GRAPH_HTTP_DISABLE`: when set to `true`, the service does not listen on HTTP and only consumes events (defaults to `false`)
|
||||
* `GRAPH_EVENTS_DISABLE_CONSUMER`: when set to `true`, the service does not consome events and only listens on HTTP (defaults to `false`)
|
||||
|
||||
## Download URLs
|
||||
|
||||
`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.
|
||||
|
||||
## Metrics
|
||||
|
||||
Metrics are disabled by default, and must be enabled using the following environment variables:
|
||||
|
||||
@@ -6,6 +6,7 @@ package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
|
||||
"github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
|
||||
@@ -175,3 +176,49 @@ func (_c *BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call) RunAndReturn(run
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetDriveItemsDownloadURL provides a mock function for the type BaseGraphProvider
|
||||
func (_mock *BaseGraphProvider) SetDriveItemsDownloadURL(r *http.Request, items []libregraph.DriveItem) {
|
||||
_mock.Called(r, items)
|
||||
return
|
||||
}
|
||||
|
||||
// BaseGraphProvider_SetDriveItemsDownloadURL_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDriveItemsDownloadURL'
|
||||
type BaseGraphProvider_SetDriveItemsDownloadURL_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SetDriveItemsDownloadURL is a helper method to define mock.On call
|
||||
// - r *http.Request
|
||||
// - items []libregraph.DriveItem
|
||||
func (_e *BaseGraphProvider_Expecter) SetDriveItemsDownloadURL(r interface{}, items interface{}) *BaseGraphProvider_SetDriveItemsDownloadURL_Call {
|
||||
return &BaseGraphProvider_SetDriveItemsDownloadURL_Call{Call: _e.mock.On("SetDriveItemsDownloadURL", r, items)}
|
||||
}
|
||||
|
||||
func (_c *BaseGraphProvider_SetDriveItemsDownloadURL_Call) Run(run func(r *http.Request, items []libregraph.DriveItem)) *BaseGraphProvider_SetDriveItemsDownloadURL_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 *http.Request
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(*http.Request)
|
||||
}
|
||||
var arg1 []libregraph.DriveItem
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].([]libregraph.DriveItem)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *BaseGraphProvider_SetDriveItemsDownloadURL_Call) Return() *BaseGraphProvider_SetDriveItemsDownloadURL_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *BaseGraphProvider_SetDriveItemsDownloadURL_Call) RunAndReturn(run func(r *http.Request, items []libregraph.DriveItem)) *BaseGraphProvider_SetDriveItemsDownloadURL_Call {
|
||||
_c.Run(run)
|
||||
return _c
|
||||
}
|
||||
@@ -416,6 +416,9 @@ func (api DrivesDriveItemApi) GetDriveItem(w http.ResponseWriter, r *http.Reques
|
||||
ErrDriveItemConversion.Render(w, r)
|
||||
return
|
||||
}
|
||||
if driveItemPropertySelected(r, _selectDownloadURL) {
|
||||
api.baseGraphService.SetDriveItemsDownloadURL(r, driveItems)
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, driveItems[0])
|
||||
|
||||
@@ -980,6 +980,27 @@ var _ = Describe("DrivesDriveItemApi", func() {
|
||||
jsonData := gjson.Get(w.Body.String(), "error")
|
||||
Expect(jsonData.Get("code").String() + ": " + jsonData.Get("message").String()).To(Equal(svc.ErrDriveItemConversion.Error()))
|
||||
})
|
||||
|
||||
It("adds the download url when selected via $select", func() {
|
||||
baseGraphProvider.
|
||||
EXPECT().
|
||||
CS3ReceivedSharesToDriveItems(mock.Anything, mock.Anything).
|
||||
Return([]libregraph.DriveItem{{}}, nil).
|
||||
Once()
|
||||
baseGraphProvider.
|
||||
EXPECT().
|
||||
SetDriveItemsDownloadURL(mock.Anything, mock.Anything).
|
||||
Return().
|
||||
Once()
|
||||
|
||||
r = httptest.NewRequest(http.MethodGet, "/?$select=@microsoft.graph.downloadUrl", nil).
|
||||
WithContext(
|
||||
context.WithValue(context.Background(), chi.RouteCtxKey, rCTX),
|
||||
)
|
||||
|
||||
drivesDriveItemApi.GetDriveItem(w, r)
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
})
|
||||
})
|
||||
|
||||
It("successfully returns the share", func() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/signedurl"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
|
||||
@@ -39,6 +41,7 @@ import (
|
||||
type BaseGraphProvider interface {
|
||||
CS3ReceivedSharesToDriveItems(ctx context.Context, receivedShares []*collaboration.ReceivedShare) ([]libregraph.DriveItem, error)
|
||||
CS3ReceivedOCMSharesToDriveItems(ctx context.Context, receivedOCMShares []*ocm.ReceivedShare) ([]libregraph.DriveItem, error)
|
||||
SetDriveItemsDownloadURL(r *http.Request, items []libregraph.DriveItem)
|
||||
}
|
||||
|
||||
// BaseGraphService implements a couple of helper functions that are
|
||||
@@ -50,6 +53,7 @@ type BaseGraphService struct {
|
||||
config *config.Config
|
||||
availableRoles []*libregraph.UnifiedRoleDefinition
|
||||
publicBaseURL *url.URL
|
||||
downloadSigner signedurl.Signer
|
||||
}
|
||||
|
||||
// webURLForResource returns the public web URL pointing at the given resource
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
libregraph "github.com/opencloud-eu/libre-graph-api-go"
|
||||
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
|
||||
)
|
||||
|
||||
const downloadURLTTL = 30 * time.Minute
|
||||
|
||||
// ErrDownloadURLSigningNotConfigured is returned when no url signing secret is configured
|
||||
var ErrDownloadURLSigningNotConfigured = errors.New("download url signing is not configured")
|
||||
|
||||
// GetDriveItemContent redirects to a signed download url for a file
|
||||
func (g Graph) GetDriveItemContent(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
driveID, err := parseIDParam(r, "driveID")
|
||||
if err != nil {
|
||||
errorcode.RenderError(w, r, err)
|
||||
return
|
||||
}
|
||||
itemID, err := parseIDParam(r, "itemID")
|
||||
if err != nil {
|
||||
errorcode.RenderError(w, r, err)
|
||||
return
|
||||
}
|
||||
if driveID.GetStorageId() != itemID.GetStorageId() || driveID.GetSpaceId() != itemID.GetSpaceId() {
|
||||
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Item does not exist")
|
||||
return
|
||||
}
|
||||
|
||||
user, ok := revactx.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "user not in context")
|
||||
return
|
||||
}
|
||||
|
||||
gatewayClient, err := g.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
stat, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: &itemID}})
|
||||
switch {
|
||||
case err != nil:
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
case stat.GetStatus().GetCode() == cs3rpc.Code_CODE_OK:
|
||||
case stat.GetStatus().GetCode() == cs3rpc.Code_CODE_NOT_FOUND:
|
||||
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, stat.GetStatus().GetMessage())
|
||||
return
|
||||
case stat.GetStatus().GetCode() == cs3rpc.Code_CODE_PERMISSION_DENIED:
|
||||
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, stat.GetStatus().GetMessage())
|
||||
return
|
||||
case stat.GetStatus().GetCode() == cs3rpc.Code_CODE_UNAUTHENTICATED:
|
||||
errorcode.Unauthenticated.Render(w, r, http.StatusUnauthorized, stat.GetStatus().GetMessage())
|
||||
return
|
||||
default:
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, stat.GetStatus().GetMessage())
|
||||
return
|
||||
}
|
||||
if stat.GetInfo().GetType() != storageprovider.ResourceType_RESOURCE_TYPE_FILE {
|
||||
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Item is not a file")
|
||||
return
|
||||
}
|
||||
|
||||
downloadURL, err := g.signedDownloadURL(&itemID, user.GetId().GetOpaqueId())
|
||||
if err != nil {
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, downloadURL, http.StatusFound)
|
||||
}
|
||||
|
||||
// SetDriveItemsDownloadURL adds a signed download url to every file in items when requested via $select
|
||||
func (g BaseGraphService) SetDriveItemsDownloadURL(r *http.Request, items []libregraph.DriveItem) {
|
||||
if !g.downloadURLRequested(r) {
|
||||
return
|
||||
}
|
||||
user, ok := revactx.ContextGetUser(r.Context())
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for i := range items {
|
||||
g.signDriveItemDownloadURL(&items[i], user.GetId().GetOpaqueId())
|
||||
}
|
||||
}
|
||||
|
||||
func (g BaseGraphService) setDriveItemDownloadURL(r *http.Request, item *libregraph.DriveItem) {
|
||||
if !g.downloadURLRequested(r) {
|
||||
return
|
||||
}
|
||||
user, ok := revactx.ContextGetUser(r.Context())
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
g.signDriveItemDownloadURL(item, user.GetId().GetOpaqueId())
|
||||
}
|
||||
|
||||
func (g BaseGraphService) signDriveItemDownloadURL(item *libregraph.DriveItem, userID string) {
|
||||
if item.File == nil {
|
||||
return
|
||||
}
|
||||
id, err := storagespace.ParseID(item.GetId())
|
||||
if err != nil {
|
||||
g.logger.Debug().Err(err).Str("id", item.GetId()).Msg("could not parse drive item id for the download url")
|
||||
return
|
||||
}
|
||||
u, err := g.signedDownloadURL(&id, userID)
|
||||
if err != nil {
|
||||
g.logger.Debug().Err(err).Str("id", item.GetId()).Msg("could not sign the download url")
|
||||
return
|
||||
}
|
||||
item.MicrosoftGraphDownloadUrl = &u
|
||||
}
|
||||
|
||||
func (g BaseGraphService) signedDownloadURL(id *storageprovider.ResourceId, userID string) (string, error) {
|
||||
if g.downloadSigner == nil {
|
||||
return "", ErrDownloadURLSigningNotConfigured
|
||||
}
|
||||
base, err := g.getWebDavBaseURL()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
base.Path = path.Join(base.Path, storagespace.FormatResourceID(id))
|
||||
return g.downloadSigner.Sign(base.String(), userID, downloadURLTTL)
|
||||
}
|
||||
|
||||
func (g BaseGraphService) downloadURLRequested(r *http.Request) bool {
|
||||
return g.downloadSigner != nil && driveItemPropertySelected(r, _selectDownloadURL)
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package svc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/go-chi/chi/v5"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/signedurl"
|
||||
cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/shared"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/mocks"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults"
|
||||
identitymocks "github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks"
|
||||
"github.com/opencloud-eu/opencloud/services/graph/pkg/metrics"
|
||||
service "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0"
|
||||
)
|
||||
|
||||
const urlSigningSecret = "url-signing-secret"
|
||||
|
||||
// verifySignedDownloadURL checks the signature of a download url for the given user and returns the url
|
||||
func verifySignedDownloadURL(signed, userID string) *url.URL {
|
||||
GinkgoHelper()
|
||||
|
||||
verifier, err := signedurl.NewJWTSignedURL(signedurl.WithSecret(urlSigningSecret))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
subject, err := verifier.Verify(signed)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(subject).To(Equal(userID))
|
||||
|
||||
u, err := url.Parse(signed)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
return u
|
||||
}
|
||||
|
||||
var _ = Describe("GetDriveItemContent", func() {
|
||||
const (
|
||||
driveID = "storageid$spaceid"
|
||||
itemID = "storageid$spaceid!nodeid"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx context.Context
|
||||
gatewayClient *cs3mocks.GatewayAPIClient
|
||||
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
eventsPublisher mocks.Publisher
|
||||
rr *httptest.ResponseRecorder
|
||||
fileInfo *provider.ResourceInfo
|
||||
|
||||
currentUser = &userpb.User{Id: &userpb.UserId{OpaqueId: "user"}}
|
||||
)
|
||||
|
||||
newService := func(signingSecret string) service.Service {
|
||||
logger := log.NewLogger()
|
||||
metrics, _ := metrics.New(prometheus.NewRegistry(), &logger, func([]string) (string, string) { return "", "" })
|
||||
|
||||
cfg := defaults.FullDefaultConfig()
|
||||
cfg.Identity.LDAP.CACert = ""
|
||||
cfg.TokenManager.JWTSecret = "loremipsum"
|
||||
cfg.Commons = &shared.Commons{URLSigningSecret: signingSecret}
|
||||
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
|
||||
|
||||
svc, err := service.NewService(
|
||||
service.Config(cfg),
|
||||
service.Metrics(metrics),
|
||||
service.WithGatewaySelector(gatewaySelector),
|
||||
service.EventsPublisher(&eventsPublisher),
|
||||
service.WithIdentityBackend(&identitymocks.Backend{}),
|
||||
)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
return svc
|
||||
}
|
||||
|
||||
newRequest := func(withUser bool) *http.Request {
|
||||
r := httptest.NewRequest(http.MethodGet, "/graph/v1beta1/drives/"+driveID+"/items/"+itemID+"/content", nil)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("driveID", driveID)
|
||||
rctx.URLParams.Add("itemID", itemID)
|
||||
c := ctx
|
||||
if withUser {
|
||||
c = revactx.ContextSetUser(c, currentUser)
|
||||
}
|
||||
return r.WithContext(context.WithValue(c, chi.RouteCtxKey, rctx))
|
||||
}
|
||||
|
||||
BeforeEach(func() {
|
||||
eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
pool.RemoveSelector("GatewaySelector" + "eu.opencloud.api.gateway")
|
||||
gatewayClient = &cs3mocks.GatewayAPIClient{}
|
||||
gatewaySelector = pool.GetSelector[gateway.GatewayAPIClient](
|
||||
"GatewaySelector",
|
||||
"eu.opencloud.api.gateway",
|
||||
func(cc grpc.ClientConnInterface) gateway.GatewayAPIClient {
|
||||
return gatewayClient
|
||||
},
|
||||
)
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
ctx = context.Background()
|
||||
|
||||
fileInfo = &provider.ResourceInfo{
|
||||
Type: provider.ResourceType_RESOURCE_TYPE_FILE,
|
||||
Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "nodeid"},
|
||||
}
|
||||
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Info: fileInfo,
|
||||
}, nil)
|
||||
})
|
||||
|
||||
It("redirects to a signed download url", func() {
|
||||
newService(urlSigningSecret).GetDriveItemContent(rr, newRequest(true))
|
||||
Expect(rr.Code).To(Equal(http.StatusFound))
|
||||
|
||||
target := verifySignedDownloadURL(rr.Header().Get("Location"), "user")
|
||||
Expect(target.Path).To(Equal("/dav/spaces/" + itemID))
|
||||
})
|
||||
|
||||
It("returns 404 for a folder", func() {
|
||||
fileInfo.Type = provider.ResourceType_RESOURCE_TYPE_CONTAINER
|
||||
|
||||
newService(urlSigningSecret).GetDriveItemContent(rr, newRequest(true))
|
||||
Expect(rr.Code).To(Equal(http.StatusNotFound))
|
||||
})
|
||||
|
||||
It("returns 404 for an unknown item", func() {
|
||||
gatewayClient.ExpectedCalls = nil
|
||||
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{Status: status.NewNotFound(ctx, "not found")}, nil)
|
||||
|
||||
newService(urlSigningSecret).GetDriveItemContent(rr, newRequest(true))
|
||||
Expect(rr.Code).To(Equal(http.StatusNotFound))
|
||||
})
|
||||
|
||||
It("treats permission denied as not found", func() {
|
||||
gatewayClient.ExpectedCalls = nil
|
||||
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{Status: status.NewPermissionDenied(ctx, errors.New("denied"), "denied")}, nil)
|
||||
|
||||
newService(urlSigningSecret).GetDriveItemContent(rr, newRequest(true))
|
||||
Expect(rr.Code).To(Equal(http.StatusNotFound))
|
||||
})
|
||||
|
||||
It("returns 404 when the item belongs to another drive", func() {
|
||||
r := newRequest(true)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("driveID", "storageid$otherspace")
|
||||
rctx.URLParams.Add("itemID", itemID)
|
||||
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
|
||||
|
||||
newService(urlSigningSecret).GetDriveItemContent(rr, r)
|
||||
Expect(rr.Code).To(Equal(http.StatusNotFound))
|
||||
gatewayClient.AssertNotCalled(GinkgoT(), "Stat", mock.Anything, mock.Anything)
|
||||
})
|
||||
|
||||
It("returns 500 without a user in the context", func() {
|
||||
newService(urlSigningSecret).GetDriveItemContent(rr, newRequest(false))
|
||||
Expect(rr.Code).To(Equal(http.StatusInternalServerError))
|
||||
})
|
||||
|
||||
It("returns 500 when url signing is not configured", func() {
|
||||
newService("").GetDriveItemContent(rr, newRequest(true))
|
||||
Expect(rr.Code).To(Equal(http.StatusInternalServerError))
|
||||
})
|
||||
})
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
const (
|
||||
_selectAllowedValues = "@libre.graph.permissions.actions.allowedValues"
|
||||
_selectShareTypes = "@libre.graph.shareTypes"
|
||||
_selectDownloadURL = "@microsoft.graph.downloadUrl"
|
||||
)
|
||||
|
||||
// without it the provider leaves the share-types opaque empty
|
||||
@@ -266,6 +267,7 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) {
|
||||
if driveItemPropertySelected(r, _selectShareTypes) {
|
||||
g.addShareTypes(ctx, files, lRes.GetInfos())
|
||||
}
|
||||
g.SetDriveItemsDownloadURL(r, files)
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, &ListResponse{Value: files})
|
||||
@@ -330,7 +332,6 @@ func (g Graph) GetDriveItem(w http.ResponseWriter, r *http.Request) {
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if driveItemPropertySelected(r, _selectAllowedValues) {
|
||||
driveItem.LibreGraphPermissionsActionsAllowedValues = unifiedrole.CS3ResourcePermissionsToLibregraphActions(res.GetInfo().GetPermissionSet())
|
||||
}
|
||||
@@ -348,6 +349,7 @@ func (g Graph) GetDriveItem(w http.ResponseWriter, r *http.Request) {
|
||||
infos := []*storageprovider.ResourceInfo{res.GetInfo()}
|
||||
driveItem.LibreGraphShareTypes = shareTypesOf(res.GetInfo(), g.listLinkShares(ctx, infos))
|
||||
}
|
||||
g.setDriveItemDownloadURL(r, driveItem)
|
||||
|
||||
if driveItemRelationExpanded(r, _expandThumbnails) {
|
||||
setDriveItemThumbnails(driveItem, res.GetInfo(), g.config.Commons.OpenCloudURL)
|
||||
@@ -438,6 +440,7 @@ func (g Graph) listDriveItemChildren(w http.ResponseWriter, r *http.Request, dri
|
||||
if driveItemPropertySelected(r, _selectShareTypes) {
|
||||
g.addShareTypes(r.Context(), files, res.GetInfos())
|
||||
}
|
||||
g.SetDriveItemsDownloadURL(r, files)
|
||||
|
||||
g.setDriveItemsThumbnails(r, files, res.GetInfos())
|
||||
|
||||
|
||||
@@ -90,7 +90,9 @@ var _ = Describe("Driveitems", func() {
|
||||
cfg = defaults.FullDefaultConfig()
|
||||
cfg.Identity.LDAP.CACert = "" // skip the startup checks, we don't use LDAP at all in this tests
|
||||
cfg.TokenManager.JWTSecret = "loremipsum"
|
||||
cfg.Commons = &shared.Commons{}
|
||||
cfg.Commons = &shared.Commons{
|
||||
URLSigningSecret: urlSigningSecret,
|
||||
}
|
||||
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
|
||||
|
||||
var err error
|
||||
@@ -277,6 +279,70 @@ var _ = Describe("Driveitems", func() {
|
||||
unifiedrole.DriveItemContentRead,
|
||||
))
|
||||
})
|
||||
|
||||
It("adds @microsoft.graph.downloadUrl when selected via $select", func() {
|
||||
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
StorageSpaces: []*provider.StorageSpace{{Owner: currentUser, Root: &provider.ResourceId{}}},
|
||||
}, nil)
|
||||
gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Infos: []*provider.ResourceInfo{
|
||||
{
|
||||
Type: provider.ResourceType_RESOURCE_TYPE_FILE,
|
||||
Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"},
|
||||
Etag: "etag",
|
||||
Mtime: utils.TimeToTS(time.Now()),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drive/root/children?$select=@microsoft.graph.downloadUrl", nil)
|
||||
r = r.WithContext(revactx.ContextSetUser(ctx, currentUser))
|
||||
svc.GetRootDriveChildren(rr, r)
|
||||
Expect(rr.Code).To(Equal(http.StatusOK))
|
||||
data, err := io.ReadAll(rr.Body)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
res := itemsList{}
|
||||
Expect(json.Unmarshal(data, &res)).To(Succeed())
|
||||
Expect(res.Value).To(HaveLen(1))
|
||||
target := verifySignedDownloadURL(res.Value[0].GetMicrosoftGraphDownloadUrl(), "user")
|
||||
Expect(target.Path).To(Equal("/dav/spaces/storageid$spaceid!opaqueid"))
|
||||
})
|
||||
|
||||
It("honours @microsoft.graph.downloadUrl in a combined $select", func() {
|
||||
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
StorageSpaces: []*provider.StorageSpace{{Owner: currentUser, Root: &provider.ResourceId{}}},
|
||||
}, nil)
|
||||
gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Infos: []*provider.ResourceInfo{
|
||||
{
|
||||
Type: provider.ResourceType_RESOURCE_TYPE_FILE,
|
||||
Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"},
|
||||
Etag: "etag",
|
||||
Mtime: utils.TimeToTS(time.Now()),
|
||||
PermissionSet: &provider.ResourcePermissions{GetPath: true, InitiateFileDownload: true},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drive/root/children?$select=@libre.graph.permissions.actions.allowedValues,@microsoft.graph.downloadUrl", nil)
|
||||
r = r.WithContext(revactx.ContextSetUser(ctx, currentUser))
|
||||
svc.GetRootDriveChildren(rr, r)
|
||||
Expect(rr.Code).To(Equal(http.StatusOK))
|
||||
data, err := io.ReadAll(rr.Body)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
res := itemsList{}
|
||||
Expect(json.Unmarshal(data, &res)).To(Succeed())
|
||||
Expect(res.Value).To(HaveLen(1))
|
||||
Expect(res.Value[0].MicrosoftGraphDownloadUrl).ToNot(BeNil())
|
||||
Expect(res.Value[0].GetLibreGraphPermissionsActionsAllowedValues()).To(ConsistOf(
|
||||
unifiedrole.DriveItemPathRead,
|
||||
unifiedrole.DriveItemContentRead,
|
||||
))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetDriveItem", func() {
|
||||
@@ -389,6 +455,21 @@ var _ = Describe("Driveitems", func() {
|
||||
Expect(item.Children[0].Thumbnails).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
|
||||
It("adds @microsoft.graph.downloadUrl to a file when selected via $select", func() {
|
||||
folderInfo.Type = provider.ResourceType_RESOURCE_TYPE_FILE
|
||||
|
||||
Expect(getItem(newRequest("")).MicrosoftGraphDownloadUrl).To(BeNil())
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
item := getItem(newRequest("?$select=@microsoft.graph.downloadUrl"))
|
||||
target := verifySignedDownloadURL(item.GetMicrosoftGraphDownloadUrl(), "user")
|
||||
Expect(target.Path).To(Equal("/dav/spaces/storageid$spaceid!nodeid"))
|
||||
})
|
||||
|
||||
It("omits @microsoft.graph.downloadUrl for a folder when selected via $select", func() {
|
||||
Expect(getItem(newRequest("?$select=@microsoft.graph.downloadUrl")).MicrosoftGraphDownloadUrl).To(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetDriveItem $expand=children error", func() {
|
||||
@@ -510,6 +591,7 @@ var _ = Describe("Driveitems", func() {
|
||||
res := assertItemsList(1)
|
||||
Expect(res.Value[0].Audio).To(BeNil())
|
||||
Expect(res.Value[0].Location).To(BeNil())
|
||||
Expect(res.Value[0].MicrosoftGraphDownloadUrl).To(BeNil())
|
||||
Expect(res.Value[0].LibreGraphMeFollowing).To(BeNil())
|
||||
Expect(res.Value[0].LibreGraphTags).To(BeNil())
|
||||
Expect(res.Value[0].PendingOperations).To(BeNil())
|
||||
@@ -750,6 +832,60 @@ var _ = Describe("Driveitems", func() {
|
||||
Expect(res.Value[0].GetLibreGraphMeFollowing()).To(BeFalse())
|
||||
})
|
||||
|
||||
It("adds @microsoft.graph.downloadUrl to files when selected via $select", func() {
|
||||
gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Infos: []*provider.ResourceInfo{
|
||||
{
|
||||
Type: provider.ResourceType_RESOURCE_TYPE_FILE,
|
||||
Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"},
|
||||
Etag: "etag",
|
||||
Mtime: utils.TimeToTS(mtime),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
r = httptest.NewRequest(http.MethodGet, "/graph/v1.0/drives/storageid$spaceid/items/storageid$spaceid!nodeid/children?$select=@microsoft.graph.downloadUrl", nil)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("driveID", "storageid$spaceid")
|
||||
rctx.URLParams.Add("driveItemID", "storageid$spaceid!nodeid")
|
||||
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
|
||||
|
||||
res := assertItemsList(1)
|
||||
target := verifySignedDownloadURL(res.Value[0].GetMicrosoftGraphDownloadUrl(), "user")
|
||||
Expect(target.Path).To(Equal("/dav/spaces/storageid$spaceid!opaqueid"))
|
||||
})
|
||||
|
||||
It("omits @microsoft.graph.downloadUrl for folders when selected via $select", func() {
|
||||
gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Infos: []*provider.ResourceInfo{
|
||||
{
|
||||
Type: provider.ResourceType_RESOURCE_TYPE_FILE,
|
||||
Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"},
|
||||
Etag: "etag",
|
||||
Mtime: utils.TimeToTS(mtime),
|
||||
},
|
||||
{
|
||||
Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER,
|
||||
Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "folderid"},
|
||||
Etag: "etag",
|
||||
Mtime: utils.TimeToTS(mtime),
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
r = httptest.NewRequest(http.MethodGet, "/graph/v1.0/drives/storageid$spaceid/items/storageid$spaceid!nodeid/children?$select=@microsoft.graph.downloadUrl", nil)
|
||||
rctx := chi.NewRouteContext()
|
||||
rctx.URLParams.Add("driveID", "storageid$spaceid")
|
||||
rctx.URLParams.Add("driveItemID", "storageid$spaceid!nodeid")
|
||||
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
|
||||
|
||||
res := assertItemsList(2)
|
||||
Expect(res.Value[0].MicrosoftGraphDownloadUrl).ToNot(BeNil())
|
||||
Expect(res.Value[1].MicrosoftGraphDownloadUrl).To(BeNil())
|
||||
})
|
||||
|
||||
It("returns the audio facet if metadata is available", func() {
|
||||
gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/riandyrn/otelchi"
|
||||
microstore "go-micro.dev/v4/store"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/signedurl"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/store"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/roles"
|
||||
@@ -104,6 +105,7 @@ type Service interface { //nolint:interfacebloat
|
||||
GetRootDriveChildren(w http.ResponseWriter, r *http.Request)
|
||||
GetDriveItem(w http.ResponseWriter, r *http.Request)
|
||||
GetDriveItemChildren(w http.ResponseWriter, r *http.Request)
|
||||
GetDriveItemContent(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
CreateUploadSession(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
@@ -146,6 +148,16 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
|
||||
return Graph{}, fmt.Errorf("could not parse graph.spaces.webdav_base: %w", err)
|
||||
}
|
||||
|
||||
var downloadSigner signedurl.Signer
|
||||
if options.Config.Commons != nil && options.Config.Commons.URLSigningSecret != "" {
|
||||
downloadSigner, err = signedurl.NewJWTSignedURL(signedurl.WithSecret(options.Config.Commons.URLSigningSecret))
|
||||
if err != nil {
|
||||
return Graph{}, fmt.Errorf("could not create download url signer: %w", err)
|
||||
}
|
||||
} else {
|
||||
options.Logger.Warn().Msg("no url signing secret configured, driveItem download urls are disabled")
|
||||
}
|
||||
|
||||
baseGraphService := BaseGraphService{
|
||||
logger: &options.Logger,
|
||||
identityCache: identityCache,
|
||||
@@ -153,6 +165,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
|
||||
config: options.Config,
|
||||
availableRoles: unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(options.Config.UnifiedRoles.AvailableRoles...)),
|
||||
publicBaseURL: publicBaseURL,
|
||||
downloadSigner: downloadSigner,
|
||||
}
|
||||
|
||||
drivesDriveItemService, err := NewDrivesDriveItemService(options.Logger, options.GatewaySelector)
|
||||
@@ -271,6 +284,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
|
||||
r.Get("/", drivesDriveItemApi.GetDriveItem)
|
||||
r.Patch("/", drivesDriveItemApi.UpdateDriveItem)
|
||||
r.Delete("/", drivesDriveItemApi.DeleteDriveItem)
|
||||
r.Get("/content", svc.GetDriveItemContent)
|
||||
r.Post("/invite", driveItemPermissionsApi.Invite)
|
||||
r.Post("/createLink", driveItemPermissionsApi.CreateLink)
|
||||
r.Route("/permissions", func(r chi.Router) {
|
||||
|
||||
Reference in new issue
Block a user