Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions services/graph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
47 changes: 47 additions & 0 deletions services/graph/mocks/base_graph_provider.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion services/graph/pkg/service/v0/api_driveitem_permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ func (s DriveItemPermissionsService) ListPermissions(ctx context.Context, itemID

driveItems := make(driveItemsByResourceID, 1)
// we can use the statResponse to build the drive item before fetching the shares
item, err := cs3ResourceToDriveItem(s.logger, s.publicBaseURL, statResponse.GetInfo())
item, err := s.cs3ResourceToDriveItem(statResponse.GetInfo())
if err != nil {
return collectionOfPermissions, err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import (
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
"github.com/opencloud-eu/opencloud/services/graph/pkg/linktype"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)

func (s DriveItemPermissionsService) CreateLink(ctx context.Context, driveItemID *storageprovider.ResourceId, createLink libregraph.DriveItemCreateLink) (libregraph.Permission, error) {
Expand Down
3 changes: 3 additions & 0 deletions services/graph/pkg/service/v0/api_drives_drive_item.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
21 changes: 21 additions & 0 deletions services/graph/pkg/service/v0/api_drives_drive_item_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion services/graph/pkg/service/v0/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (

"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)

// ListApplications implements the Service interface.
Expand Down
2 changes: 1 addition & 1 deletion services/graph/pkg/service/v0/approleassignments.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import (

"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
settingsmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/settings/v0"
settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/utils"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
merrors "go-micro.dev/v4/errors"
)

Expand Down
24 changes: 15 additions & 9 deletions services/graph/pkg/service/v0/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"path"
"time"
Expand All @@ -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"

Expand All @@ -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
Expand All @@ -50,6 +53,16 @@ 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
// (e.g. https://cloud.example.com/f/<resource-id>), using the pre-parsed
// publicBaseURL held by the service.
func (g BaseGraphService) webURLForResource(rid *storageprovider.ResourceId) *string {
u := *g.publicBaseURL
u.Path = path.Join(u.Path, "f", storagespace.FormatResourceID(rid))
return libregraph.PtrString(u.String())
}

func (g BaseGraphService) getDriveItem(ctx context.Context, ref *storageprovider.Reference) (*libregraph.DriveItem, error) {
Expand All @@ -66,7 +79,7 @@ func (g BaseGraphService) getDriveItem(ctx context.Context, ref *storageprovider
refStr, _ := storagespace.FormatReference(ref)
return nil, fmt.Errorf("could not stat %s: %s", refStr, res.GetStatus().GetMessage())
}
return cs3ResourceToDriveItem(g.logger, g.publicBaseURL, res.GetInfo())
return g.cs3ResourceToDriveItem(res.GetInfo())
}

func (g BaseGraphService) CS3ReceivedSharesToDriveItems(ctx context.Context, receivedShares []*collaboration.ReceivedShare) ([]libregraph.DriveItem, error) {
Expand Down Expand Up @@ -217,14 +230,6 @@ func (g BaseGraphService) cs3SpacePermissionsToLibreGraph(ctx context.Context, s
}

func (g BaseGraphService) libreGraphPermissionFromCS3PublicShare(createdLink *link.PublicShare) (*libregraph.Permission, error) {
webURL, err := url.Parse(g.config.Spaces.WebDavBase)
if err != nil {
g.logger.Error().
Err(err).
Str("url", g.config.Spaces.WebDavBase).
Msg("failed to parse webURL base url")
return nil, err
}
lt, actions := linktype.SharingLinkTypeFromCS3Permissions(createdLink.GetPermissions())
perm := libregraph.NewPermission()
perm.Id = libregraph.PtrString(createdLink.GetId().GetOpaqueId())
Expand All @@ -235,6 +240,7 @@ func (g BaseGraphService) libreGraphPermissionFromCS3PublicShare(createdLink *li
LibreGraphQuickLink: libregraph.PtrBool(createdLink.GetQuicklink()),
}
perm.LibreGraphPermissionsActions = actions
webURL := *g.publicBaseURL
webURL.Path = path.Join(webURL.Path, "s", createdLink.GetToken())
perm.Link.SetWebUrl(webURL.String())

Expand Down
142 changes: 142 additions & 0 deletions services/graph/pkg/service/v0/driveitem_download.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading