mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-06 04:09:40 -06:00
Merge pull request #9686 from owncloud/ms365
feat: Microsoft office 365 Cloud and Office Online Server support
This commit is contained in:
6
changelog/unreleased/microsoft-cloud-on-prem.md
Normal file
6
changelog/unreleased/microsoft-cloud-on-prem.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Enhancement: Microsoft Office365 and Office Online support
|
||||
|
||||
Add support for Microsoft Office365 Cloud and Microsoft Office Online on premises. You can use the cloud feature either within a Microsoft [CSP](https://learn.microsoft.com/en-us/partner-center/enroll/csp-overview) partnership or via the ownCloud office365 proxy subscription.
|
||||
Please contact sales@owncloud.com to get more information about the ownCloud office365 proxy subscription.
|
||||
|
||||
https://github.com/owncloud/ocis/pull/9686
|
||||
@@ -4,6 +4,8 @@ The collaboration service connects ocis with document servers such as Collabora
|
||||
|
||||
Since this service requires an external document server, it won't start by default when using `ocis server`. You must start it manually with the `ocis collaboration server` command.
|
||||
|
||||
This service needs to be part of the ocis service mesh. It is not intended to be used as a standalone service. You must share the common config variables like OCIS_URL, OCIS_JWT_SECRET and OCIS_REVA_GATEWAY betweed this service and the other ocis services. In addition to that, MICRO_REGISTRY_ADDRESS should point to the main ocis service registry.
|
||||
|
||||
## Requirements
|
||||
|
||||
The collaboration service requires the target document server (ONLYOFFICE, Collabora, etc.) to be up and running. Additionally, some Infinite Scale services are also required to be running in order to register the GRPC service for the `open in app` action in the webUI. The following internal and external services need to be available:
|
||||
@@ -18,6 +20,9 @@ If any of the named services above have not been started or are not reachable, t
|
||||
|
||||
There are a few variables that you need to set:
|
||||
|
||||
* `COLLABORATION_APP_NAME`:\
|
||||
The name of the connected WebOffice app, either `Collabora`, `OnlyOffice`, `Microsoft365` or `MicrosoftOfficeOnline`.
|
||||
|
||||
* `COLLABORATION_APP_ADDR`:\
|
||||
The URL of the collaborative editing app (onlyoffice, collabora, etc).\
|
||||
For example: `https://office.example.com`.
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
|
||||
connector "github.com/owncloud/ocis/v2/services/collaboration/pkg/connector"
|
||||
|
||||
http "net/http"
|
||||
|
||||
io "io"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
@@ -25,17 +27,17 @@ func (_m *ContentConnectorService) EXPECT() *ContentConnectorService_Expecter {
|
||||
return &ContentConnectorService_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// GetFile provides a mock function with given fields: ctx, writer
|
||||
func (_m *ContentConnectorService) GetFile(ctx context.Context, writer io.Writer) error {
|
||||
ret := _m.Called(ctx, writer)
|
||||
// GetFile provides a mock function with given fields: ctx, w
|
||||
func (_m *ContentConnectorService) GetFile(ctx context.Context, w http.ResponseWriter) error {
|
||||
ret := _m.Called(ctx, w)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetFile")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, io.Writer) error); ok {
|
||||
r0 = rf(ctx, writer)
|
||||
if rf, ok := ret.Get(0).(func(context.Context, http.ResponseWriter) error); ok {
|
||||
r0 = rf(ctx, w)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
@@ -50,14 +52,14 @@ type ContentConnectorService_GetFile_Call struct {
|
||||
|
||||
// GetFile is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - writer io.Writer
|
||||
func (_e *ContentConnectorService_Expecter) GetFile(ctx interface{}, writer interface{}) *ContentConnectorService_GetFile_Call {
|
||||
return &ContentConnectorService_GetFile_Call{Call: _e.mock.On("GetFile", ctx, writer)}
|
||||
// - w http.ResponseWriter
|
||||
func (_e *ContentConnectorService_Expecter) GetFile(ctx interface{}, w interface{}) *ContentConnectorService_GetFile_Call {
|
||||
return &ContentConnectorService_GetFile_Call{Call: _e.mock.On("GetFile", ctx, w)}
|
||||
}
|
||||
|
||||
func (_c *ContentConnectorService_GetFile_Call) Run(run func(ctx context.Context, writer io.Writer)) *ContentConnectorService_GetFile_Call {
|
||||
func (_c *ContentConnectorService_GetFile_Call) Run(run func(ctx context.Context, w http.ResponseWriter)) *ContentConnectorService_GetFile_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(io.Writer))
|
||||
run(args[0].(context.Context), args[1].(http.ResponseWriter))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
@@ -67,7 +69,7 @@ func (_c *ContentConnectorService_GetFile_Call) Return(_a0 error) *ContentConnec
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ContentConnectorService_GetFile_Call) RunAndReturn(run func(context.Context, io.Writer) error) *ContentConnectorService_GetFile_Call {
|
||||
func (_c *ContentConnectorService_GetFile_Call) RunAndReturn(run func(context.Context, http.ResponseWriter) error) *ContentConnectorService_GetFile_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ type App struct {
|
||||
Addr string `yaml:"addr" env:"COLLABORATION_APP_ADDR" desc:"The URL where the WOPI app is located, such as https://127.0.0.1:8080." introductionVersion:"6.0.0"`
|
||||
Insecure bool `yaml:"insecure" env:"COLLABORATION_APP_INSECURE" desc:"Skip TLS certificate verification when connecting to the WOPI app" introductionVersion:"6.0.0"`
|
||||
|
||||
ProofKeys ProofKeys `yaml:"proofkeys"`
|
||||
ProofKeys ProofKeys `yaml:"proofkeys"`
|
||||
LicenseCheckEnable bool `yaml:"licensecheckenable" env:"COLLABORATION_APP_LICENSE_CHECK_ENABLE" desc:"Enable license check for edit. Needs to be enabled when using Microsoft365 with the business flow." introductionVersion:"%%NEXT%%"`
|
||||
}
|
||||
|
||||
type ProofKeys struct {
|
||||
|
||||
@@ -4,5 +4,7 @@ package config
|
||||
type Wopi struct {
|
||||
WopiSrc string `yaml:"wopisrc" env:"COLLABORATION_WOPI_SRC" desc:"The WOPISrc base URL containing schema, host and port. Set this to the schema and domain where the collaboration service is reachable for the wopi app, such as https://office.owncloud.test." introductionVersion:"6.0.0"`
|
||||
Secret string `yaml:"secret" env:"COLLABORATION_WOPI_SECRET" desc:"Used to mint and verify WOPI JWT tokens and encrypt and decrypt the REVA JWT token embedded in the WOPI JWT token." introductionVersion:"6.0.0"`
|
||||
DisableChat bool `yaml:"disable_chat" env:"COLLABORATION_WOPI_DISABLE_CHAT;OCIS_WOPI_DISABLE_CHAT" desc:"Disable chat in the frontend." introductionVersion:"%%NEXT%%"`
|
||||
DisableChat bool `yaml:"disable_chat" env:"COLLABORATION_WOPI_DISABLE_CHAT;OCIS_WOPI_DISABLE_CHAT" desc:"Disable chat in the frontend. This feature is available in OnlyOffice and Microsoft." introductionVersion:"%%NEXT%%"`
|
||||
ProxyURL string `yaml:"proxy_url" env:"COLLABORATION_WOPI_PROXY_URL" desc:"The URL to the ownCloud Office365 WOPI proxy. Optional. To use this feature, you need an office365 proxy subscription. If you become part of the Microsoft CSP program (https://learn.microsoft.com/en-us/partner-center/enroll/csp-overview), you can use the WebOffice without a proxy." introductionVersion:"%%NEXT%%"`
|
||||
ProxySecret string `yaml:"proxy_secret" env:"COLLABORATION_WOPI_PROXY_SECRET" desc:"The secret to authenticate against the ownCloud Office365 WOPI proxy. Optional. This secret can be obtained from ownCloud via the office365 proxy subscription." introductionVersion:"%%NEXT%%"`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package connector
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
)
|
||||
|
||||
// ConnectorResponse represent a response from the FileConnectorService.
|
||||
// The ConnectorResponse is oriented to HTTP, so it has the Status, Headers
|
||||
// and Body that the actual HTTP response should have. This includes HTTP
|
||||
@@ -33,6 +39,48 @@ func NewResponseWithLock(status int, lockID string) *ConnectorResponse {
|
||||
}
|
||||
}
|
||||
|
||||
// NewResponseLockConflict creates a new ConnectorResponse with the status 409
|
||||
// and the "X-WOPI-Lock" header having the value in the lockID parameter.
|
||||
//
|
||||
// This is used for conflict responses where the current lock id needs
|
||||
// to be returned, although the `GetLock` method also uses this method for a
|
||||
// successful response (with the lock id included)
|
||||
// The lockFailureReason parameter will be included in the "X-WOPI-LockFailureReason".
|
||||
func NewResponseLockConflict(lockID string, lockFailureReason string) *ConnectorResponse {
|
||||
return &ConnectorResponse{
|
||||
Status: 409,
|
||||
Headers: map[string]string{
|
||||
HeaderWopiLock: lockID,
|
||||
HeaderWopiLockFailureReason: lockFailureReason,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewResponseWithVersion creates a new ConnectorResponse with the specified status
|
||||
// and the "X-WOPI-ItemVersion" header having the value in the mtime parameter.
|
||||
func NewResponseWithVersion(mtime *types.Timestamp) *ConnectorResponse {
|
||||
return &ConnectorResponse{
|
||||
Status: 200,
|
||||
Headers: map[string]string{
|
||||
HeaderWopiVersion: getVersion(mtime),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewResponseWithVersionAndLock creates a new ConnectorResponse with the specified status
|
||||
// and the "X-WOPI-ItemVersion" header and the "X-WOPI-Lock" header
|
||||
// having the values in the mtime and lockID parameters.
|
||||
func NewResponseWithVersionAndLock(status int, mtime *types.Timestamp, lockID string) *ConnectorResponse {
|
||||
r := &ConnectorResponse{
|
||||
Status: status,
|
||||
Headers: map[string]string{
|
||||
HeaderWopiVersion: getVersion(mtime),
|
||||
HeaderWopiLock: lockID,
|
||||
},
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// NewResponseSuccessBody creates a new ConnectorResponse with a fixed 200
|
||||
// (success) status and the specified body. The headers will be nil.
|
||||
//
|
||||
@@ -136,3 +184,9 @@ func (c *Connector) GetFileConnector() FileConnectorService {
|
||||
func (c *Connector) GetContentConnector() ContentConnectorService {
|
||||
return c.contentConnector
|
||||
}
|
||||
|
||||
// getVersion returns a string representation of the timestamp
|
||||
func getVersion(timestamp *types.Timestamp) string {
|
||||
return "v" + strconv.FormatUint(timestamp.GetSeconds(), 10) +
|
||||
strconv.FormatUint(uint64(timestamp.GetNanos()), 10)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
// Target file is within the WOPI context
|
||||
type ContentConnectorService interface {
|
||||
// GetFile downloads the file and write its contents in the provider writer
|
||||
GetFile(ctx context.Context, writer io.Writer) error
|
||||
GetFile(ctx context.Context, w http.ResponseWriter) error
|
||||
// PutFile uploads the stream up to the stream length. The file should be
|
||||
// locked beforehand, so the lockID needs to be provided.
|
||||
// The current lockID will be returned ONLY if a conflict happens (the file is
|
||||
@@ -61,9 +61,11 @@ func NewContentConnector(gwc gatewayv1beta1.GatewayAPIClient, cfg *config.Config
|
||||
// You can pass a pre-configured zerologger instance through the context that
|
||||
// will be used to log messages.
|
||||
//
|
||||
// The contents of the file will be written directly into the writer passed as
|
||||
// The contents of the file will be written directly into the http Response writer passed as
|
||||
// parameter.
|
||||
func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error {
|
||||
// Be aware that the body of the response will be written during the execution of this method.
|
||||
// Any further modifications to the response headers or body will be ignored.
|
||||
func (c *ContentConnector) GetFile(ctx context.Context, w http.ResponseWriter) error {
|
||||
wopiContext, err := middleware.WopiContextFromCtx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -74,6 +76,16 @@ func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error
|
||||
Logger()
|
||||
logger.Debug().Msg("GetFile: start")
|
||||
|
||||
sResp, err := c.gwc.Stat(ctx, &providerv1beta1.StatRequest{
|
||||
Ref: wopiContext.FileReference,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("GetFile: Stat Request failed")
|
||||
return err
|
||||
}
|
||||
if sResp.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK {
|
||||
return NewConnectorError(500, sResp.GetStatus().GetCode().String()+" "+sResp.GetStatus().GetMessage())
|
||||
}
|
||||
// Initiate download request
|
||||
req := &providerv1beta1.InitiateFileDownloadRequest{
|
||||
Ref: wopiContext.FileReference,
|
||||
@@ -168,13 +180,14 @@ func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error
|
||||
return NewConnectorError(500, "GetFile: Downloading the file failed")
|
||||
}
|
||||
|
||||
w.Header().Set(HeaderWopiVersion, getVersion(sResp.GetInfo().GetMtime()))
|
||||
|
||||
// Copy the download into the writer
|
||||
_, err = io.Copy(writer, httpResp.Body)
|
||||
_, err = io.Copy(w, httpResp.Body)
|
||||
if err != nil {
|
||||
logger.Error().Msg("GetFile: copying the file content to the response body failed")
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debug().Msg("GetFile: success")
|
||||
return nil
|
||||
}
|
||||
@@ -199,6 +212,8 @@ func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error
|
||||
// lock ID that should be used in the X-WOPI-Lock header. In other error
|
||||
// cases or if the method is successful, an empty string will be returned
|
||||
// (check for err != nil to know if something went wrong)
|
||||
//
|
||||
// On success, the method will return the new mtime of the file
|
||||
func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, streamLength int64, lockID string) (*ConnectorResponse, error) {
|
||||
wopiContext, err := middleware.WopiContextFromCtx(ctx)
|
||||
if err != nil {
|
||||
@@ -230,13 +245,14 @@ func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, stream
|
||||
return NewResponse(500), nil
|
||||
}
|
||||
|
||||
mtime := statRes.GetInfo().GetMtime()
|
||||
// If there is a lock and it mismatches, return 409
|
||||
if statRes.GetInfo().GetLock() != nil && statRes.GetInfo().GetLock().GetLockId() != lockID {
|
||||
logger.Error().
|
||||
Str("LockID", statRes.GetInfo().GetLock().GetLockId()).
|
||||
Msg("PutFile: wrong lock")
|
||||
// onlyoffice says it's required to send the current lockId, MS doesn't say anything
|
||||
return NewResponseWithLock(409, statRes.GetInfo().GetLock().GetLockId()), nil
|
||||
return NewResponseLockConflict(statRes.GetInfo().GetLock().GetLockId(), "Lock Mismatch"), nil
|
||||
}
|
||||
|
||||
// only unlocked uploads can go through if the target file is empty,
|
||||
@@ -246,7 +262,7 @@ func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, stream
|
||||
if lockID == "" && statRes.GetInfo().GetLock() == nil && statRes.GetInfo().GetSize() > 0 {
|
||||
logger.Error().Msg("PutFile: file must be locked first")
|
||||
// onlyoffice says to send an empty string if the file is unlocked, MS doesn't say anything
|
||||
return NewResponseWithLock(409, ""), nil
|
||||
return NewResponseLockConflict("", "Cannot PutFile on unlocked file"), nil
|
||||
}
|
||||
|
||||
// Prepare the data to initiate the upload
|
||||
@@ -367,8 +383,25 @@ func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, stream
|
||||
Msg("UploadHelper: Put request to the upload endpoint failed with unexpected status")
|
||||
return NewResponse(500), nil
|
||||
}
|
||||
// We need a stat call on the target file after the upload to get the
|
||||
// new mtime
|
||||
statResAfter, err := c.gwc.Stat(ctx, &providerv1beta1.StatRequest{
|
||||
Ref: wopiContext.FileReference,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("PutFile: stat after upload failed")
|
||||
return nil, err
|
||||
}
|
||||
if statResAfter.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK {
|
||||
logger.Error().
|
||||
Str("StatusCode", statRes.GetStatus().GetCode().String()).
|
||||
Str("StatusMsg", statRes.GetStatus().GetMessage()).
|
||||
Msg("PutFile: stat after upload failed with unexpected status")
|
||||
return NewResponse(500), nil
|
||||
}
|
||||
mtime = statResAfter.GetInfo().GetMtime()
|
||||
}
|
||||
|
||||
logger.Debug().Msg("PutFile: success")
|
||||
return NewResponse(200), nil
|
||||
return NewResponseWithVersion(mtime), nil
|
||||
}
|
||||
|
||||
@@ -6,14 +6,15 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
revactx "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
|
||||
@@ -52,7 +53,6 @@ var _ = Describe("ContentConnector", func() {
|
||||
},
|
||||
Path: ".",
|
||||
},
|
||||
User: &userv1beta1.User{}, // Not used for now
|
||||
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
|
||||
}
|
||||
|
||||
@@ -77,15 +77,28 @@ var _ = Describe("ContentConnector", func() {
|
||||
})
|
||||
|
||||
Describe("GetFile", func() {
|
||||
BeforeEach(func() {
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).Return(&providerv1beta1.StatResponse{
|
||||
Status: status.NewOK(context.Background()),
|
||||
Info: &providerv1beta1.ResourceInfo{
|
||||
Id: &providerv1beta1.ResourceId{
|
||||
StorageId: "abc",
|
||||
OpaqueId: "12345",
|
||||
SpaceId: "zzz",
|
||||
},
|
||||
Path: ".",
|
||||
},
|
||||
}, nil)
|
||||
})
|
||||
It("No valid context", func() {
|
||||
sb := &strings.Builder{}
|
||||
sb := httptest.NewRecorder()
|
||||
ctx := context.Background()
|
||||
err := cc.GetFile(ctx, sb)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("Initiate download failed", func() {
|
||||
sb := &strings.Builder{}
|
||||
sb := httptest.NewRecorder()
|
||||
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
|
||||
|
||||
targetErr := errors.New("Something went wrong")
|
||||
@@ -98,7 +111,7 @@ var _ = Describe("ContentConnector", func() {
|
||||
})
|
||||
|
||||
It("Initiate download status not ok", func() {
|
||||
sb := &strings.Builder{}
|
||||
sb := httptest.NewRecorder()
|
||||
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
|
||||
|
||||
gatewayClient.On("InitiateFileDownload", mock.Anything, mock.Anything).Times(1).Return(&gateway.InitiateFileDownloadResponse{
|
||||
@@ -112,7 +125,7 @@ var _ = Describe("ContentConnector", func() {
|
||||
})
|
||||
|
||||
It("Missing download endpoint", func() {
|
||||
sb := &strings.Builder{}
|
||||
sb := httptest.NewRecorder()
|
||||
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
|
||||
|
||||
gatewayClient.On("InitiateFileDownload", mock.Anything, mock.Anything).Times(1).Return(&gateway.InitiateFileDownloadResponse{
|
||||
@@ -126,7 +139,7 @@ var _ = Describe("ContentConnector", func() {
|
||||
})
|
||||
|
||||
It("Download request failed", func() {
|
||||
sb := &strings.Builder{}
|
||||
sb := httptest.NewRecorder()
|
||||
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
|
||||
|
||||
gatewayClient.On("InitiateFileDownload", mock.Anything, mock.Anything).Times(1).Return(&gateway.InitiateFileDownloadResponse{
|
||||
@@ -149,7 +162,7 @@ var _ = Describe("ContentConnector", func() {
|
||||
})
|
||||
|
||||
It("Download request success", func() {
|
||||
sb := &strings.Builder{}
|
||||
sb := httptest.NewRecorder()
|
||||
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
|
||||
|
||||
gatewayClient.On("InitiateFileDownload", mock.Anything, mock.Anything).Times(1).Return(&gateway.InitiateFileDownloadResponse{
|
||||
@@ -167,11 +180,11 @@ var _ = Describe("ContentConnector", func() {
|
||||
Expect(srvReqHeader.Get("X-Access-Token")).To(Equal(wopiCtx.AccessToken))
|
||||
Expect(srvReqHeader.Get("X-Reva-Transfer")).To(Equal("MyDownloadToken"))
|
||||
Expect(err).To(Succeed())
|
||||
Expect(sb.String()).To(Equal(randomContent))
|
||||
Expect(sb.Body.String()).To(Equal(randomContent))
|
||||
})
|
||||
|
||||
It("ViewOnlyMode Download request success", func() {
|
||||
sb := &strings.Builder{}
|
||||
sb := httptest.NewRecorder()
|
||||
|
||||
wopiCtx = middleware.WopiContext{
|
||||
AccessToken: "abcdef123456",
|
||||
@@ -184,7 +197,6 @@ var _ = Describe("ContentConnector", func() {
|
||||
},
|
||||
Path: ".",
|
||||
},
|
||||
User: &userv1beta1.User{}, // Not used for now
|
||||
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY,
|
||||
}
|
||||
|
||||
@@ -208,7 +220,7 @@ var _ = Describe("ContentConnector", func() {
|
||||
Expect(srvReqHeader.Get("X-Access-Token")).To(Equal(wopiCtx.ViewOnlyToken))
|
||||
Expect(srvReqHeader.Get("X-Reva-Transfer")).To(Equal("MyDownloadToken"))
|
||||
Expect(err).To(Succeed())
|
||||
Expect(sb.String()).To(Equal(randomContent))
|
||||
Expect(sb.Body.String()).To(Equal(randomContent))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -244,7 +256,7 @@ var _ = Describe("ContentConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := cc.PutFile(ctx, reader, reader.Size(), "notARandomLockId")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -264,9 +276,11 @@ var _ = Describe("ContentConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := cc.PutFile(ctx, reader, reader.Size(), "notARandomLockId")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(409))
|
||||
Expect(response.Headers).To(HaveLen(2))
|
||||
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("goodAndValidLock"))
|
||||
Expect(response.Headers[connector.HeaderWopiLockFailureReason]).To(Equal("Lock Mismatch"))
|
||||
})
|
||||
|
||||
It("Upload without lockId but on a non empty file", func() {
|
||||
@@ -282,7 +296,7 @@ var _ = Describe("ContentConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := cc.PutFile(ctx, reader, reader.Size(), "")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(409))
|
||||
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal(""))
|
||||
})
|
||||
@@ -332,7 +346,7 @@ var _ = Describe("ContentConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -348,7 +362,8 @@ var _ = Describe("ContentConnector", func() {
|
||||
LockId: "goodAndValidLock",
|
||||
Type: providerv1beta1.LockType_LOCK_TYPE_WRITE,
|
||||
},
|
||||
Size: uint64(123456789),
|
||||
Size: uint64(123456789),
|
||||
Mtime: utils.TimeToTS(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
}, nil)
|
||||
|
||||
@@ -357,9 +372,10 @@ var _ = Describe("ContentConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
Expect(response.Headers).To(HaveLen(1))
|
||||
Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v16094592000"))
|
||||
})
|
||||
|
||||
It("Missing upload endpoint", func() {
|
||||
@@ -382,7 +398,7 @@ var _ = Describe("ContentConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock")
|
||||
Expect(err).To(BeNil())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -414,7 +430,7 @@ var _ = Describe("ContentConnector", func() {
|
||||
|
||||
response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock")
|
||||
Expect(srvReqHeader.Get("X-Access-Token")).To(Equal(wopiCtx.AccessToken))
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -434,6 +450,23 @@ var _ = Describe("ContentConnector", func() {
|
||||
},
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).Times(1).Return(&providerv1beta1.StatResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Info: &providerv1beta1.ResourceInfo{
|
||||
Lock: &providerv1beta1.Lock{
|
||||
LockId: "goodAndValidLock",
|
||||
Type: providerv1beta1.LockType_LOCK_TYPE_WRITE,
|
||||
},
|
||||
Size: uint64(123456789),
|
||||
Id: &providerv1beta1.ResourceId{
|
||||
StorageId: "storageID",
|
||||
OpaqueId: "opaqueID",
|
||||
SpaceId: "spaceID",
|
||||
},
|
||||
Mtime: utils.TimeToTS(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
}, nil)
|
||||
|
||||
gatewayClient.On("InitiateFileUpload", mock.Anything, mock.Anything).Times(1).Return(&gateway.InitiateFileUploadResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Protocols: []*gateway.FileUploadProtocol{
|
||||
@@ -446,9 +479,10 @@ var _ = Describe("ContentConnector", func() {
|
||||
|
||||
response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock")
|
||||
Expect(srvReqHeader.Get("X-Access-Token")).To(Equal(wopiCtx.AccessToken))
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
Expect(response.Headers).To(HaveLen(1))
|
||||
Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v16094592000"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"io"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -19,12 +18,15 @@ import (
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"github.com/cs3org/reva/v2/pkg/storagespace"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/connector/fileinfo"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/helpers"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/middleware"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/wopisrc"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -52,7 +54,7 @@ type FileConnectorService interface {
|
||||
// needs to be provided.
|
||||
// The current lockID will be returned if a conflict happens
|
||||
RefreshLock(ctx context.Context, lockID string) (*ConnectorResponse, error)
|
||||
// Unlock will unlock the target file. The current lockID needs to be
|
||||
// UnLock will unlock the target file. The current lockID needs to be
|
||||
// provided.
|
||||
// The current lockID will be returned if a conflict happens
|
||||
UnLock(ctx context.Context, lockID string) (*ConnectorResponse, error)
|
||||
@@ -172,6 +174,8 @@ func (f *FileConnector) GetLock(ctx context.Context) (*ConnectorResponse, error)
|
||||
// the method will return an empty lock id.
|
||||
//
|
||||
// For the "unlock and relock" operation, the behavior will be the same.
|
||||
//
|
||||
// On success, the mtime of the file will be returned in the X-Wopi-Version header.
|
||||
func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (*ConnectorResponse, error) {
|
||||
wopiContext, err := middleware.WopiContextFromCtx(ctx)
|
||||
if err != nil {
|
||||
@@ -236,11 +240,26 @@ func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (*Co
|
||||
setOrRefreshStatus = resp.GetStatus()
|
||||
}
|
||||
|
||||
statResp, err := f.gwc.Stat(ctx, &providerv1beta1.StatRequest{
|
||||
Ref: wopiContext.FileReference,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Lock failed trying to get the file info")
|
||||
return nil, err
|
||||
}
|
||||
if statResp.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK {
|
||||
logger.Error().
|
||||
Str("StatusCode", statResp.GetStatus().GetCode().String()).
|
||||
Str("StatusMsg", statResp.GetStatus().GetMessage()).
|
||||
Msg("Lock failed trying to get the file info with unexpected status")
|
||||
return NewResponse(500), nil
|
||||
}
|
||||
|
||||
// we're checking the status of either the "SetLock" or "RefreshLock" operations
|
||||
switch setOrRefreshStatus.GetCode() {
|
||||
case rpcv1beta1.Code_CODE_OK:
|
||||
logger.Debug().Msg("SetLock successful")
|
||||
return NewResponse(200), nil
|
||||
return NewResponseWithVersion(statResp.GetInfo().GetMtime()), nil
|
||||
|
||||
case rpcv1beta1.Code_CODE_FAILED_PRECONDITION, rpcv1beta1.Code_CODE_ABORTED:
|
||||
// Code_CODE_FAILED_PRECONDITION -> Lock operation mismatched lock
|
||||
@@ -270,7 +289,7 @@ func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (*Co
|
||||
logger.Warn().
|
||||
Str("LockID", resp.GetLock().GetLockId()).
|
||||
Msg("SetLock conflict")
|
||||
return NewResponseWithLock(409, resp.GetLock().GetLockId()), nil
|
||||
return NewResponseLockConflict(resp.GetLock().GetLockId(), "Conflicting LockID"), nil
|
||||
}
|
||||
|
||||
// TODO: according to the spec we need to treat this as a RefreshLock
|
||||
@@ -281,7 +300,7 @@ func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (*Co
|
||||
logger.Warn().
|
||||
Str("LockID", resp.GetLock().GetLockId()).
|
||||
Msg("SetLock lock refreshed instead")
|
||||
return NewResponse(200), nil // no need to send the lockID for a 200 code
|
||||
return NewResponseWithVersion(statResp.GetInfo().GetMtime()), nil
|
||||
}
|
||||
|
||||
logger.Error().Msg("SetLock failed and could not refresh")
|
||||
@@ -313,6 +332,8 @@ func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (*Co
|
||||
// return an empty lock id.
|
||||
// The conflict happens if the provided lockID doesn't match the one actually
|
||||
// applied in the target file.
|
||||
//
|
||||
// On success, the mtime of the file will be returned in the X-Wopi-Version header.
|
||||
func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (*ConnectorResponse, error) {
|
||||
wopiContext, err := middleware.WopiContextFromCtx(ctx)
|
||||
if err != nil {
|
||||
@@ -347,10 +368,27 @@ func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (*Connec
|
||||
return nil, err
|
||||
}
|
||||
|
||||
statResp, err := f.gwc.Stat(ctx, &providerv1beta1.StatRequest{
|
||||
Ref: wopiContext.FileReference,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("RefreshLock failed trying to get the file info")
|
||||
return nil, err
|
||||
}
|
||||
if statResp.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK {
|
||||
logger.Error().
|
||||
Str("StatusCode", statResp.GetStatus().GetCode().String()).
|
||||
Str("StatusMsg", statResp.GetStatus().GetMessage()).
|
||||
Msg("RefreshLock failed trying to get the file info with unexpected status")
|
||||
return NewResponse(500), nil
|
||||
}
|
||||
|
||||
switch resp.GetStatus().GetCode() {
|
||||
case rpcv1beta1.Code_CODE_OK:
|
||||
logger.Debug().Msg("RefreshLock successful")
|
||||
return NewResponse(200), nil
|
||||
// The current lock should not be returned in the headers on success
|
||||
// https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/refreshlock#response-headers
|
||||
return NewResponseWithVersion(statResp.GetInfo().GetMtime()), nil
|
||||
|
||||
case rpcv1beta1.Code_CODE_NOT_FOUND:
|
||||
logger.Error().
|
||||
@@ -390,7 +428,7 @@ func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (*Connec
|
||||
Str("StatusCode", resp.GetStatus().GetCode().String()).
|
||||
Str("StatusMsg", resp.GetStatus().GetMessage()).
|
||||
Msg("RefreshLock failed, no lock on file")
|
||||
return NewResponseWithLock(409, ""), nil
|
||||
return NewResponseLockConflict("", "No lock on file"), nil
|
||||
} else {
|
||||
// lock is different than the one requested, otherwise we wouldn't reached this point
|
||||
logger.Error().
|
||||
@@ -398,7 +436,7 @@ func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (*Connec
|
||||
Str("StatusCode", resp.GetStatus().GetCode().String()).
|
||||
Str("StatusMsg", resp.GetStatus().GetMessage()).
|
||||
Msg("RefreshLock failed, lock mismatch")
|
||||
return NewResponseWithLock(409, resp.GetLock().GetLockId()), nil
|
||||
return NewResponseLockConflict(resp.GetLock().GetLockId(), "Lock mismatch"), nil
|
||||
}
|
||||
default:
|
||||
logger.Error().
|
||||
@@ -422,6 +460,8 @@ func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (*Connec
|
||||
// return an empty lock id.
|
||||
// The conflict happens if the provided lockID doesn't match the one actually
|
||||
// applied in the target file.
|
||||
//
|
||||
// On success, the mtime of the file will be returned in the X-Wopi-Version header.
|
||||
func (f *FileConnector) UnLock(ctx context.Context, lockID string) (*ConnectorResponse, error) {
|
||||
wopiContext, err := middleware.WopiContextFromCtx(ctx)
|
||||
if err != nil {
|
||||
@@ -452,14 +492,29 @@ func (f *FileConnector) UnLock(ctx context.Context, lockID string) (*ConnectorRe
|
||||
return nil, err
|
||||
}
|
||||
|
||||
statResp, err := f.gwc.Stat(ctx, &providerv1beta1.StatRequest{
|
||||
Ref: wopiContext.FileReference,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Unlock failed trying to get the file info")
|
||||
return nil, err
|
||||
}
|
||||
if statResp.GetStatus().GetCode() != rpcv1beta1.Code_CODE_OK {
|
||||
logger.Error().
|
||||
Str("StatusCode", statResp.GetStatus().GetCode().String()).
|
||||
Str("StatusMsg", statResp.GetStatus().GetMessage()).
|
||||
Msg("Unlock failed trying to get the file info with unexpected status")
|
||||
return NewResponse(500), nil
|
||||
}
|
||||
|
||||
switch resp.GetStatus().GetCode() {
|
||||
case rpcv1beta1.Code_CODE_OK:
|
||||
logger.Debug().Msg("Unlock successful")
|
||||
return NewResponse(200), nil
|
||||
return NewResponseWithVersion(statResp.GetInfo().GetMtime()), nil
|
||||
case rpcv1beta1.Code_CODE_ABORTED:
|
||||
// File isn't locked. Need to return 409 with empty lock
|
||||
logger.Error().Err(err).Msg("Unlock failed, file isn't locked")
|
||||
return NewResponseWithLock(409, ""), nil
|
||||
return NewResponseLockConflict("", "File isn't locked"), nil
|
||||
case rpcv1beta1.Code_CODE_LOCKED:
|
||||
// We need to return 409 with the current lock
|
||||
req := &providerv1beta1.GetLockRequest{
|
||||
@@ -496,7 +551,7 @@ func (f *FileConnector) UnLock(ctx context.Context, lockID string) (*ConnectorRe
|
||||
Msg("Unlock failed, lock mismatch")
|
||||
outLockId = resp.GetLock().GetLockId()
|
||||
}
|
||||
return NewResponseWithLock(409, outLockId), nil
|
||||
return NewResponseLockConflict(outLockId, "Lock mismatch"), nil
|
||||
default:
|
||||
logger.Error().
|
||||
Str("StatusCode", resp.GetStatus().GetCode().String()).
|
||||
@@ -612,7 +667,7 @@ func (f *FileConnector) PutRelativeFileSuggested(ctx context.Context, ccs Conten
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wopiSrcURL, err := f.generateWOPISrc(ctx, wopiContext, newLogger)
|
||||
wopiSrcURL, err := f.generateWOPISrc(wopiContext, newLogger)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("PutRelativeFileSuggested: error generating the WOPISrc parameter")
|
||||
return nil, err
|
||||
@@ -708,7 +763,7 @@ func (f *FileConnector) PutRelativeFileRelative(ctx context.Context, ccs Content
|
||||
}
|
||||
// if conflict generate a different name and retry.
|
||||
// this should happen only once
|
||||
wopiSrcURL, err2 := f.generateWOPISrc(ctx, wopiContext, newLogger)
|
||||
wopiSrcURL, err2 := f.generateWOPISrc(wopiContext, newLogger)
|
||||
if err2 != nil {
|
||||
newLogger.Error().
|
||||
Err(err2).
|
||||
@@ -724,12 +779,13 @@ func (f *FileConnector) PutRelativeFileRelative(ctx context.Context, ccs Content
|
||||
Str("LockID", lockID).
|
||||
Msg("PutRelativeFileRelative: error conflict")
|
||||
|
||||
// need to build the response ourselves
|
||||
// need to build the response ourselves
|
||||
return &ConnectorResponse{
|
||||
Status: 409,
|
||||
Headers: map[string]string{
|
||||
HeaderWopiValidRT: finalTarget,
|
||||
HeaderWopiLock: lockID,
|
||||
HeaderWopiValidRT: finalTarget,
|
||||
HeaderWopiLock: lockID,
|
||||
HeaderWopiLockFailureReason: "Lock Conflict",
|
||||
},
|
||||
Body: map[string]interface{}{
|
||||
"Name": target,
|
||||
@@ -747,7 +803,7 @@ func (f *FileConnector) PutRelativeFileRelative(ctx context.Context, ccs Content
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wopiSrcURL, err := f.generateWOPISrc(ctx, wopiContext, newLogger)
|
||||
wopiSrcURL, err := f.generateWOPISrc(wopiContext, newLogger)
|
||||
if err != nil {
|
||||
newLogger.Error().Err(err).Msg("PutRelativeFileRelative: error generating the WOPISrc parameter")
|
||||
return nil, err
|
||||
@@ -798,7 +854,8 @@ func (f *FileConnector) DeleteFile(ctx context.Context, lockID string) (*Connect
|
||||
if deleteRes.GetStatus().GetCode() == rpcv1beta1.Code_CODE_TOO_EARLY {
|
||||
// starting from 20ms, double the waiting time for each retry
|
||||
// capping at 5 secs
|
||||
waitingTime := (20 * time.Millisecond) << retries
|
||||
var waitingTime time.Duration
|
||||
waitingTime = (20 * time.Millisecond) << retries
|
||||
if waitingTime.Seconds() > 5 {
|
||||
waitingTime = 5 * time.Second
|
||||
}
|
||||
@@ -849,7 +906,7 @@ func (f *FileConnector) DeleteFile(ctx context.Context, lockID string) (*Connect
|
||||
logger.Error().
|
||||
Str("LockID", resp.GetLock().GetLockId()).
|
||||
Msg("DeleteFile: file is locked")
|
||||
return NewResponseWithLock(409, resp.GetLock().GetLockId()), nil
|
||||
return NewResponseLockConflict(resp.GetLock().GetLockId(), "File is locked"), nil
|
||||
} else {
|
||||
// return the original error since the file isn't locked
|
||||
logger.Error().Msg("DeleteFile: delete failed on unlocked file")
|
||||
@@ -942,7 +999,7 @@ func (f *FileConnector) RenameFile(ctx context.Context, lockID, target string) (
|
||||
Str("StatusCode", moveRes.GetStatus().GetCode().String()).
|
||||
Str("StatusMsg", moveRes.GetStatus().GetMessage()).
|
||||
Msg("RenameFile: conflict")
|
||||
return NewResponseWithLock(409, currentLockID), nil
|
||||
return NewResponseLockConflict(currentLockID, "Lock Conflict"), nil
|
||||
}
|
||||
|
||||
if moveRes.GetStatus().GetCode() == rpcv1beta1.Code_CODE_ALREADY_EXISTS {
|
||||
@@ -1018,7 +1075,6 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
|
||||
}
|
||||
|
||||
hexEncodedOwnerId := hex.EncodeToString([]byte(statRes.GetInfo().GetOwner().GetOpaqueId() + "@" + statRes.GetInfo().GetOwner().GetIdp()))
|
||||
version := strconv.FormatUint(statRes.GetInfo().GetMtime().GetSeconds(), 10) + "." + strconv.FormatUint(uint64(statRes.GetInfo().GetMtime().GetNanos()), 10)
|
||||
|
||||
// UserId must use only alphanumeric chars (https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/files/checkfileinfo/checkfileinfo-response#requirements-for-user-identity-properties)
|
||||
// assign userId, userFriendlyName and isAnonymousUser
|
||||
@@ -1029,30 +1085,44 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
|
||||
isAnonymousUser := true
|
||||
|
||||
isPublicShare := false
|
||||
if wopiContext.User != nil {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
if user.String() != "" {
|
||||
// if we have a wopiContext.User
|
||||
isPublicShare = utils.ExistsInOpaque(wopiContext.User.GetOpaque(), "public-share-role")
|
||||
isPublicShare = utils.ExistsInOpaque(user.GetOpaque(), "public-share-role")
|
||||
if !isPublicShare {
|
||||
hexEncodedWopiUserId := hex.EncodeToString([]byte(wopiContext.User.GetId().GetOpaqueId() + "@" + wopiContext.User.GetId().GetIdp()))
|
||||
hexEncodedWopiUserId := hex.EncodeToString([]byte(user.GetId().GetOpaqueId() + "@" + user.GetId().GetIdp()))
|
||||
isAnonymousUser = false
|
||||
userFriendlyName = wopiContext.User.GetDisplayName()
|
||||
userFriendlyName = user.GetDisplayName()
|
||||
userId = hexEncodedWopiUserId
|
||||
}
|
||||
}
|
||||
|
||||
breadcrumbFolderName := path.Dir(statRes.Info.Path)
|
||||
if breadcrumbFolderName == "." || breadcrumbFolderName == "" {
|
||||
breadcrumbFolderName = statRes.GetInfo().GetSpace().GetName()
|
||||
}
|
||||
|
||||
ocisUrl, err := url.Parse(f.cfg.Commons.OcisURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
breadcrumbFolderURL, viewAppUrl, editAppUrl := *ocisUrl, *ocisUrl, *ocisUrl
|
||||
breadcrumbFolderURL.Path = path.Join(breadcrumbFolderURL.Path, "f", storagespace.FormatResourceID(statRes.GetInfo().GetId()))
|
||||
viewAppUrl.Path = path.Join(viewAppUrl.Path, "external"+strings.ToLower(f.cfg.App.Name))
|
||||
editAppUrl.Path = path.Join(editAppUrl.Path, "external"+strings.ToLower(f.cfg.App.Name))
|
||||
// fileinfo map
|
||||
infoMap := map[string]interface{}{
|
||||
fileinfo.KeyOwnerID: hexEncodedOwnerId,
|
||||
fileinfo.KeySize: int64(statRes.GetInfo().GetSize()),
|
||||
fileinfo.KeyVersion: version,
|
||||
fileinfo.KeyVersion: getVersion(statRes.GetInfo().GetMtime()),
|
||||
fileinfo.KeyBaseFileName: path.Base(statRes.GetInfo().GetPath()),
|
||||
fileinfo.KeyBreadcrumbDocName: path.Base(statRes.GetInfo().GetPath()),
|
||||
// to get the folder we actually need to do a GetPath() request
|
||||
//BreadcrumbFolderName: path.Dir(statRes.Info.Path),
|
||||
fileinfo.KeyBreadcrumbFolderName: breadcrumbFolderName,
|
||||
fileinfo.KeyBreadcrumbFolderURL: breadcrumbFolderURL.String(),
|
||||
|
||||
// TODO: these URLs must point to ocis, which is hosting the editor's iframe
|
||||
//fileinfo.KeyHostViewURL: wopiContext.ViewAppUrl,
|
||||
//fileinfo.KeyHostEditURL: wopiContext.EditAppUrl,
|
||||
//fileinfo.KeyHostViewURL: viewAppUrl.String(),
|
||||
//fileinfo.KeyHostEditURL: editAppUrl.String(),
|
||||
|
||||
fileinfo.KeyEnableOwnerTermination: true, // only for collabora
|
||||
fileinfo.KeySupportsExtendedLockLength: true,
|
||||
@@ -1066,7 +1136,8 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
|
||||
fileinfo.KeyUserFriendlyName: userFriendlyName,
|
||||
fileinfo.KeyUserID: userId,
|
||||
|
||||
fileinfo.KeyPostMessageOrigin: f.cfg.Commons.OcisURL,
|
||||
fileinfo.KeyPostMessageOrigin: f.cfg.Commons.OcisURL,
|
||||
fileinfo.KeyLicenseCheckForEditIsEnabled: f.cfg.App.LicenseCheckEnable,
|
||||
}
|
||||
|
||||
switch wopiContext.ViewMode {
|
||||
@@ -1082,7 +1153,7 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
|
||||
infoMap[fileinfo.KeyDisableCopy] = true // only for collabora
|
||||
infoMap[fileinfo.KeyDisablePrint] = true
|
||||
if !isPublicShare {
|
||||
infoMap[fileinfo.KeyWatermarkText] = f.watermarkText(wopiContext.User) // only for collabora
|
||||
infoMap[fileinfo.KeyWatermarkText] = f.watermarkText(user) // only for collabora
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1162,7 +1233,7 @@ func (f *FileConnector) generatePrefix() string {
|
||||
// contains the resource id of the target file without the path
|
||||
// (storage, opaque and space points directly to the file). The path component
|
||||
// will be ignored
|
||||
func (f *FileConnector) generateWOPISrc(ctx context.Context, wopiContext middleware.WopiContext, logger zerolog.Logger) (*url.URL, error) {
|
||||
func (f *FileConnector) generateWOPISrc(wopiContext middleware.WopiContext, logger zerolog.Logger) (*url.URL, error) {
|
||||
// get the WOPI token for the new file
|
||||
accessToken, _, err := middleware.GenerateWopiToken(wopiContext, f.cfg)
|
||||
if err != nil {
|
||||
@@ -1174,16 +1245,14 @@ func (f *FileConnector) generateWOPISrc(ctx context.Context, wopiContext middlew
|
||||
fileRef := helpers.HashResourceId(wopiContext.FileReference.GetResourceId())
|
||||
|
||||
// generate the URL for the WOPI app to access the new created file
|
||||
wopiSrcURL, err := url.Parse(f.cfg.Wopi.WopiSrc)
|
||||
wopiSrcURL, err := wopisrc.GenerateWopiSrc(fileRef, f.cfg)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("generateWOPISrc: failed to generate WOPISrc URL for the new file")
|
||||
return nil, err
|
||||
}
|
||||
wopiSrcURL.Path = path.Join("wopi", "files", fileRef)
|
||||
q := wopiSrcURL.Query()
|
||||
q.Add("access_token", accessToken)
|
||||
wopiSrcURL.RawQuery = q.Encode()
|
||||
|
||||
return wopiSrcURL, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
|
||||
cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
@@ -63,25 +64,6 @@ var _ = Describe("FileConnector", func() {
|
||||
},
|
||||
Path: ".",
|
||||
},
|
||||
User: &userv1beta1.User{
|
||||
Id: &userv1beta1.UserId{
|
||||
Idp: "inmemory",
|
||||
OpaqueId: "opaqueId",
|
||||
Type: userv1beta1.UserType_USER_TYPE_PRIMARY,
|
||||
},
|
||||
Username: "Shaft",
|
||||
DisplayName: "Pet Shaft",
|
||||
Mail: "shaft@example.com",
|
||||
// Opaque is here for reference, not used by default but might be needed for some tests
|
||||
//Opaque: &typesv1beta1.Opaque{
|
||||
// Map: map[string]*typesv1beta1.OpaqueEntry{
|
||||
// "public-share-role": &typesv1beta1.OpaqueEntry{
|
||||
// Decoder: "plain",
|
||||
// Value: []byte("viewer"),
|
||||
// },
|
||||
// },
|
||||
//},
|
||||
},
|
||||
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
|
||||
}
|
||||
})
|
||||
@@ -116,7 +98,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := fc.GetLock(ctx)
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(404))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -134,7 +116,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := fc.GetLock(ctx)
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
|
||||
})
|
||||
@@ -153,7 +135,7 @@ var _ = Describe("FileConnector", func() {
|
||||
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
|
||||
|
||||
response, err := fc.Lock(ctx, "", "")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(400))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -179,10 +161,25 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewOK(ctx),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(
|
||||
&providerv1beta1.StatResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Info: &providerv1beta1.ResourceInfo{
|
||||
Mtime: &typesv1beta1.Timestamp{
|
||||
Seconds: 12345,
|
||||
Nanos: 6789,
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
response, err := fc.Lock(ctx, "abcdef123", "")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
Expect(response.Headers).To(HaveLen(1))
|
||||
Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v123456789"))
|
||||
})
|
||||
|
||||
It("Set lock mismatches error getting lock", func() {
|
||||
@@ -197,6 +194,9 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewInternal(ctx, "lock mismatch"),
|
||||
}, targetErr)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.Lock(ctx, "abcdef123", "")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(Equal(targetErr))
|
||||
@@ -218,10 +218,15 @@ var _ = Describe("FileConnector", func() {
|
||||
},
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.Lock(ctx, "abcdef123", "")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(409))
|
||||
Expect(response.Headers).To(HaveLen(2))
|
||||
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
|
||||
Expect(response.Headers[connector.HeaderWopiLockFailureReason]).To(Equal("Conflicting LockID"))
|
||||
})
|
||||
|
||||
It("Set lock mismatches but get lock matches", func() {
|
||||
@@ -239,10 +244,19 @@ var _ = Describe("FileConnector", func() {
|
||||
},
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Info: &providerv1beta1.ResourceInfo{
|
||||
Mtime: &typesv1beta1.Timestamp{Seconds: uint64(12345), Nanos: uint32(6789)},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
response, err := fc.Lock(ctx, "abcdef123", "")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
Expect(response.Headers).To(HaveLen(1))
|
||||
Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v123456789"))
|
||||
})
|
||||
|
||||
It("Set lock mismatches but get lock doesn't return lockId", func() {
|
||||
@@ -256,8 +270,11 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewOK(ctx),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.Lock(ctx, "abcdef123", "")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -269,8 +286,11 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewNotFound(ctx, "file not found"),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.Lock(ctx, "abcdef123", "")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(404))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -282,8 +302,11 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewInsufficientStorage(ctx, nil, "file too big"),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.Lock(ctx, "abcdef123", "")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -301,7 +324,7 @@ var _ = Describe("FileConnector", func() {
|
||||
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
|
||||
|
||||
response, err := fc.Lock(ctx, "", "oldLock")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(400))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -327,10 +350,19 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewOK(ctx),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Info: &providerv1beta1.ResourceInfo{
|
||||
Mtime: &typesv1beta1.Timestamp{Seconds: uint64(12345), Nanos: uint32(6789)},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
response, err := fc.Lock(ctx, "abcdef123", "oldLock")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
Expect(response.Headers).To(HaveLen(1))
|
||||
Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v123456789"))
|
||||
})
|
||||
|
||||
It("Refresh lock mismatches error getting lock", func() {
|
||||
@@ -345,6 +377,9 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewInternal(ctx, "lock mismatch"),
|
||||
}, targetErr)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.Lock(ctx, "abcdef123", "112233")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(Equal(targetErr))
|
||||
@@ -366,8 +401,11 @@ var _ = Describe("FileConnector", func() {
|
||||
},
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.Lock(ctx, "abcdef123", "112233")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(409))
|
||||
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
|
||||
})
|
||||
@@ -387,10 +425,19 @@ var _ = Describe("FileConnector", func() {
|
||||
},
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Info: &providerv1beta1.ResourceInfo{
|
||||
Mtime: &typesv1beta1.Timestamp{Seconds: uint64(12345), Nanos: uint32(6789)},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
response, err := fc.Lock(ctx, "abcdef123", "112233")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
Expect(response.Headers).To(HaveLen(1))
|
||||
Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v123456789"))
|
||||
})
|
||||
|
||||
It("Refresh lock mismatches but get lock doesn't return lockId", func() {
|
||||
@@ -404,8 +451,11 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewOK(ctx),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.Lock(ctx, "abcdef123", "112233")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -417,8 +467,11 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewNotFound(ctx, "file not found"),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.Lock(ctx, "abcdef123", "112233")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(404))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -430,8 +483,11 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewInsufficientStorage(ctx, nil, "file too big"),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.Lock(ctx, "abcdef123", "112233")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -441,7 +497,8 @@ var _ = Describe("FileConnector", func() {
|
||||
Describe("RefreshLock", func() {
|
||||
It("No valid context", func() {
|
||||
ctx := context.Background()
|
||||
response, err := fc.RefreshLock(ctx, "newLock")
|
||||
|
||||
response, err := fc.RefreshLock(ctx, "")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(response).To(BeNil())
|
||||
})
|
||||
@@ -450,7 +507,7 @@ var _ = Describe("FileConnector", func() {
|
||||
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
|
||||
|
||||
response, err := fc.RefreshLock(ctx, "")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(400))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -476,10 +533,19 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewOK(ctx),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Info: &providerv1beta1.ResourceInfo{
|
||||
Mtime: &typesv1beta1.Timestamp{Seconds: uint64(12345), Nanos: uint32(6789)},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
response, err := fc.RefreshLock(ctx, "abcdef123")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
Expect(response.Headers).To(HaveLen(1))
|
||||
Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v123456789"))
|
||||
})
|
||||
|
||||
It("Refresh lock file not found", func() {
|
||||
@@ -489,8 +555,11 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewNotFound(ctx, "file not found"),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.RefreshLock(ctx, "abcdef123")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(404))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -507,6 +576,9 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewConflict(ctx, nil, "lock mismatch"),
|
||||
}, targetErr)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.RefreshLock(ctx, "abcdef123")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(Equal(targetErr))
|
||||
@@ -524,8 +596,11 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewInternal(ctx, "lock mismatch"),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.RefreshLock(ctx, "abcdef123")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -541,8 +616,11 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewOK(ctx),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.RefreshLock(ctx, "abcdef123")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(409))
|
||||
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal(""))
|
||||
})
|
||||
@@ -562,8 +640,11 @@ var _ = Describe("FileConnector", func() {
|
||||
},
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.RefreshLock(ctx, "abcdef123")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(409))
|
||||
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
|
||||
})
|
||||
@@ -575,8 +656,11 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewInsufficientStorage(ctx, nil, "file too big"),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.RefreshLock(ctx, "abcdef123")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -585,7 +669,8 @@ var _ = Describe("FileConnector", func() {
|
||||
Describe("Unlock", func() {
|
||||
It("No valid context", func() {
|
||||
ctx := context.Background()
|
||||
response, err := fc.UnLock(ctx, "newLock")
|
||||
|
||||
response, err := fc.UnLock(ctx, "")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(response).To(BeNil())
|
||||
})
|
||||
@@ -594,7 +679,7 @@ var _ = Describe("FileConnector", func() {
|
||||
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
|
||||
|
||||
response, err := fc.UnLock(ctx, "")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(400))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -620,10 +705,19 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewOK(ctx),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Info: &providerv1beta1.ResourceInfo{
|
||||
Mtime: &typesv1beta1.Timestamp{Seconds: uint64(12345), Nanos: uint32(6789)},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
response, err := fc.UnLock(ctx, "abcdef123")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
Expect(response.Headers).To(HaveLen(1))
|
||||
Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v123456789"))
|
||||
})
|
||||
|
||||
It("Unlock file isn't locked", func() {
|
||||
@@ -633,8 +727,16 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewConflict(ctx, nil, "lock mismatch"),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Info: &providerv1beta1.ResourceInfo{
|
||||
Mtime: &typesv1beta1.Timestamp{Seconds: uint64(12345), Nanos: uint32(6789)},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
response, err := fc.UnLock(ctx, "abcdef123")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(409))
|
||||
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal(""))
|
||||
})
|
||||
@@ -651,6 +753,9 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewInternal(ctx, "something failed"),
|
||||
}, targetErr)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.UnLock(ctx, "abcdef123")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err).To(Equal(targetErr))
|
||||
@@ -668,8 +773,11 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewInternal(ctx, "something failed"),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.UnLock(ctx, "abcdef123")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -685,8 +793,11 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewOK(ctx),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.UnLock(ctx, "abcdef123")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(409))
|
||||
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal(""))
|
||||
})
|
||||
@@ -706,8 +817,11 @@ var _ = Describe("FileConnector", func() {
|
||||
},
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.UnLock(ctx, "abcdef123")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(409))
|
||||
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
|
||||
})
|
||||
@@ -719,8 +833,11 @@ var _ = Describe("FileConnector", func() {
|
||||
Status: status.NewInsufficientStorage(ctx, nil, "file too big"),
|
||||
}, nil)
|
||||
|
||||
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
|
||||
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
|
||||
|
||||
response, err := fc.UnLock(ctx, "abcdef123")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -759,7 +876,7 @@ var _ = Describe("FileConnector", func() {
|
||||
|
||||
stream := strings.NewReader("This is the content of a file")
|
||||
response, err := fc.PutRelativeFileSuggested(ctx, ccs, stream, int64(stream.Len()), "newFile.txt")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
Expect(response.Body).To(BeNil())
|
||||
@@ -813,7 +930,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := fc.PutRelativeFileSuggested(ctx, ccs, stream, int64(stream.Len()), "newDocument.docx")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
rBody := response.Body.(map[string]interface{})
|
||||
@@ -869,7 +986,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := fc.PutRelativeFileSuggested(ctx, ccs, stream, int64(stream.Len()), ".pdf")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
rBody := response.Body.(map[string]interface{})
|
||||
@@ -938,7 +1055,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := fc.PutRelativeFileSuggested(ctx, ccs, stream, int64(stream.Len()), ".pdf")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
rBody := response.Body.(map[string]interface{})
|
||||
@@ -972,7 +1089,7 @@ var _ = Describe("FileConnector", func() {
|
||||
ccs.On("PutFile", mock.Anything, stream, int64(stream.Len()), "").Times(1).Return(connector.NewResponse(500), nil)
|
||||
|
||||
response, err := fc.PutRelativeFileSuggested(ctx, ccs, stream, int64(stream.Len()), ".pdf")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
Expect(response.Body).To(BeNil())
|
||||
@@ -1012,7 +1129,7 @@ var _ = Describe("FileConnector", func() {
|
||||
|
||||
stream := strings.NewReader("This is the content of a file")
|
||||
response, err := fc.PutRelativeFileRelative(ctx, ccs, stream, int64(stream.Len()), "newFile.txt")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
Expect(response.Body).To(BeNil())
|
||||
@@ -1065,7 +1182,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := fc.PutRelativeFileRelative(ctx, ccs, stream, int64(stream.Len()), "newDocument.docx")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
rBody := response.Body.(map[string]interface{})
|
||||
@@ -1124,7 +1241,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := fc.PutRelativeFileRelative(ctx, ccs, stream, int64(stream.Len()), "convFile.pdf")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(409))
|
||||
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
|
||||
Expect(response.Headers[connector.HeaderWopiValidRT]).To(MatchRegexp(`[a-zA-Z0-9_-] convFile\.pdf`))
|
||||
@@ -1159,7 +1276,7 @@ var _ = Describe("FileConnector", func() {
|
||||
ccs.On("PutFile", mock.Anything, stream, int64(stream.Len()), "").Times(1).Return(connector.NewResponse(500), nil)
|
||||
|
||||
response, err := fc.PutRelativeFileRelative(ctx, ccs, stream, int64(stream.Len()), "convFile.pdf")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
Expect(response.Body).To(BeNil())
|
||||
@@ -1223,7 +1340,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, targetErr)
|
||||
|
||||
response, err := fc.DeleteFile(ctx, "newlock")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(404))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -1240,7 +1357,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := fc.DeleteFile(ctx, "newlock")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -1261,7 +1378,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := fc.DeleteFile(ctx, "newlock")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(409))
|
||||
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
|
||||
})
|
||||
@@ -1278,7 +1395,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := fc.DeleteFile(ctx, "newlock")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -1291,7 +1408,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := fc.DeleteFile(ctx, "newlock")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
})
|
||||
@@ -1327,7 +1444,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := fc.RenameFile(ctx, "lockid", "newFile.doc")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
Expect(response.Body).To(BeNil())
|
||||
@@ -1375,7 +1492,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := fc.RenameFile(ctx, "lockid", "newFile.doc")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
Expect(response.Body).To(BeNil())
|
||||
@@ -1399,7 +1516,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := fc.RenameFile(ctx, "lockid", "newFile.doc")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(409))
|
||||
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
|
||||
Expect(response.Body).To(BeNil())
|
||||
@@ -1442,7 +1559,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil).Once()
|
||||
|
||||
response, err := fc.RenameFile(ctx, "zzz999", "newFile.doc")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
rBody := response.Body.(map[string]interface{})
|
||||
@@ -1474,7 +1591,7 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil).Once()
|
||||
|
||||
response, err := fc.RenameFile(ctx, "zzz999", "newFile.doc")
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Headers).To(BeNil())
|
||||
rBody := response.Body.(map[string]interface{})
|
||||
@@ -1512,13 +1629,21 @@ var _ = Describe("FileConnector", func() {
|
||||
}, nil)
|
||||
|
||||
response, err := fc.CheckFileInfo(ctx)
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(500))
|
||||
Expect(response.Body).To(BeNil())
|
||||
})
|
||||
|
||||
It("Stat success", func() {
|
||||
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
|
||||
u := &userv1beta1.User{
|
||||
Id: &userv1beta1.UserId{
|
||||
Idp: "customIdp",
|
||||
OpaqueId: "admin",
|
||||
},
|
||||
DisplayName: "Pet Shaft",
|
||||
}
|
||||
ctx = ctxpkg.ContextSetUser(ctx, u)
|
||||
|
||||
gatewayClient.On("Stat", mock.Anything, mock.Anything).Times(1).Return(&providerv1beta1.StatResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
@@ -1533,17 +1658,25 @@ var _ = Describe("FileConnector", func() {
|
||||
Seconds: uint64(16273849),
|
||||
},
|
||||
Path: "/path/to/test.txt",
|
||||
// Other properties aren't used for now.
|
||||
Id: &providerv1beta1.ResourceId{
|
||||
StorageId: "storageid",
|
||||
OpaqueId: "opaqueid",
|
||||
SpaceId: "spaceid",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
expectedFileInfo := &fileinfo.Microsoft{
|
||||
OwnerID: "61616262636340637573746f6d496470", // hex of aabbcc@customIdp
|
||||
Size: int64(998877),
|
||||
Version: "16273849.0",
|
||||
BaseFileName: "test.txt",
|
||||
BreadcrumbDocName: "test.txt",
|
||||
UserCanNotWriteRelative: false,
|
||||
OwnerID: "61616262636340637573746f6d496470", // hex of aabbcc@customIdp
|
||||
Size: int64(998877),
|
||||
Version: "v162738490",
|
||||
BaseFileName: "test.txt",
|
||||
BreadcrumbDocName: "test.txt",
|
||||
BreadcrumbFolderName: "/path/to",
|
||||
BreadcrumbFolderURL: "https://ocis.example.prv/f/storageid$spaceid%21opaqueid",
|
||||
UserCanNotWriteRelative: false,
|
||||
//HostViewURL: "http://test.ex.prv/view",
|
||||
//HostEditURL: "http://test.ex.prv/edit",
|
||||
SupportsExtendedLockLength: true,
|
||||
SupportsGetLock: true,
|
||||
SupportsLocks: true,
|
||||
@@ -1552,19 +1685,20 @@ var _ = Describe("FileConnector", func() {
|
||||
SupportsRename: true,
|
||||
UserCanWrite: true,
|
||||
UserCanRename: true,
|
||||
UserID: "6f7061717565496440696e6d656d6f7279", // hex of opaqueId@inmemory
|
||||
UserID: "61646d696e40637573746f6d496470", // hex of admin@customIdp
|
||||
UserFriendlyName: "Pet Shaft",
|
||||
}
|
||||
|
||||
response, err := fc.CheckFileInfo(ctx)
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Body.(*fileinfo.Microsoft)).To(Equal(expectedFileInfo))
|
||||
})
|
||||
|
||||
It("Stat success guests", func() {
|
||||
// add user's opaque to include public-share-role
|
||||
wopiCtx.User.Opaque = &typesv1beta1.Opaque{
|
||||
u := &userv1beta1.User{}
|
||||
u.Opaque = &typesv1beta1.Opaque{
|
||||
Map: map[string]*typesv1beta1.OpaqueEntry{
|
||||
"public-share-role": &typesv1beta1.OpaqueEntry{
|
||||
Decoder: "plain",
|
||||
@@ -1576,6 +1710,7 @@ var _ = Describe("FileConnector", func() {
|
||||
wopiCtx.ViewMode = appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY
|
||||
|
||||
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
|
||||
ctx = ctxpkg.ContextSetUser(ctx, u)
|
||||
|
||||
gatewayClient.On("Stat", mock.Anything, mock.Anything).Times(1).Return(&providerv1beta1.StatResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
@@ -1590,6 +1725,11 @@ var _ = Describe("FileConnector", func() {
|
||||
Seconds: uint64(16273849),
|
||||
},
|
||||
Path: "/path/to/test.txt",
|
||||
Id: &providerv1beta1.ResourceId{
|
||||
StorageId: "storageid",
|
||||
OpaqueId: "opaqueid",
|
||||
SpaceId: "spaceid",
|
||||
},
|
||||
// Other properties aren't used for now.
|
||||
},
|
||||
}, nil)
|
||||
@@ -1625,7 +1765,7 @@ var _ = Describe("FileConnector", func() {
|
||||
response.Body.(*fileinfo.Collabora).UserID = "guest-zzz000"
|
||||
response.Body.(*fileinfo.Collabora).UserFriendlyName = "guest zzz000"
|
||||
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Body.(*fileinfo.Collabora)).To(Equal(expectedFileInfo))
|
||||
})
|
||||
@@ -1635,6 +1775,16 @@ var _ = Describe("FileConnector", func() {
|
||||
wopiCtx.ViewMode = appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY
|
||||
|
||||
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
|
||||
u := &userv1beta1.User{
|
||||
Id: &userv1beta1.UserId{
|
||||
Idp: "example.com",
|
||||
OpaqueId: "aabbcc",
|
||||
Type: userv1beta1.UserType_USER_TYPE_PRIMARY,
|
||||
},
|
||||
DisplayName: "Pet Shaft",
|
||||
Mail: "shaft@example.com",
|
||||
}
|
||||
ctx = ctxpkg.ContextSetUser(ctx, u)
|
||||
|
||||
gatewayClient.On("Stat", mock.Anything, mock.Anything).Times(1).Return(&providerv1beta1.StatResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
@@ -1649,7 +1799,11 @@ var _ = Describe("FileConnector", func() {
|
||||
Seconds: uint64(16273849),
|
||||
},
|
||||
Path: "/path/to/test.txt",
|
||||
// Other properties aren't used for now.
|
||||
Id: &providerv1beta1.ResourceId{
|
||||
StorageId: "storageid",
|
||||
OpaqueId: "opaqueid",
|
||||
SpaceId: "spaceid",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
@@ -1664,7 +1818,7 @@ var _ = Describe("FileConnector", func() {
|
||||
DisableExport: true,
|
||||
DisableCopy: true,
|
||||
DisablePrint: true,
|
||||
UserID: hex.EncodeToString([]byte("opaqueId@inmemory")),
|
||||
UserID: hex.EncodeToString([]byte("aabbcc@example.com")),
|
||||
UserFriendlyName: "Pet Shaft",
|
||||
EnableOwnerTermination: true,
|
||||
WatermarkText: "Pet Shaft shaft@example.com",
|
||||
@@ -1677,7 +1831,7 @@ var _ = Describe("FileConnector", func() {
|
||||
|
||||
response, err := fc.CheckFileInfo(ctx)
|
||||
|
||||
Expect(err).To(Succeed())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(response.Status).To(Equal(200))
|
||||
Expect(response.Body.(*fileinfo.Collabora)).To(Equal(expectedFileInfo))
|
||||
})
|
||||
|
||||
@@ -60,7 +60,7 @@ type Microsoft struct {
|
||||
// A Boolean value indicating whether the user is an education user or not.
|
||||
IsEduUser bool `json:"IsEduUser,omitempty"`
|
||||
// A Boolean value indicating whether the user is a business user or not.
|
||||
LicenseCheckForEditIsEnabled bool `json:"LicenseCheckForEditIsEnabled,omitempty"`
|
||||
LicenseCheckForEditIsEnabled bool `json:"LicenseCheckForEditIsEnabled"`
|
||||
// A string that is the name of the user, suitable for displaying in UI.
|
||||
UserFriendlyName string `json:"UserFriendlyName,omitempty"`
|
||||
// A string value containing information about the user. This string can be passed from a WOPI client to the host by means of a PutUserInfo operation. If the host has a UserInfo string for the user, they must include it in this property. See the PutUserInfo documentation for more details.
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
@@ -15,16 +14,18 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderWopiLock string = "X-WOPI-Lock"
|
||||
HeaderWopiOldLock string = "X-WOPI-OldLock"
|
||||
HeaderWopiST string = "X-WOPI-SuggestedTarget"
|
||||
HeaderWopiRT string = "X-WOPI-RelativeTarget"
|
||||
HeaderWopiOverwriteRT string = "X-WOPI-OverwriteRelativeTarget"
|
||||
HeaderWopiSize string = "X-WOPI-Size"
|
||||
HeaderWopiValidRT string = "X-WOPI-ValidRelativeTarget"
|
||||
HeaderWopiRequestedName string = "X-WOPI-RequestedName"
|
||||
HeaderContentLength string = "Content-Length"
|
||||
HeaderContentType string = "Content-Type"
|
||||
HeaderWopiLock string = "X-WOPI-Lock"
|
||||
HeaderWopiOldLock string = "X-WOPI-OldLock"
|
||||
HeaderWopiLockFailureReason string = "X-WOPI-LockFailureReason"
|
||||
HeaderWopiST string = "X-WOPI-SuggestedTarget"
|
||||
HeaderWopiRT string = "X-WOPI-RelativeTarget"
|
||||
HeaderWopiOverwriteRT string = "X-WOPI-OverwriteRelativeTarget"
|
||||
HeaderWopiSize string = "X-WOPI-Size"
|
||||
HeaderWopiValidRT string = "X-WOPI-ValidRelativeTarget"
|
||||
HeaderWopiRequestedName string = "X-WOPI-RequestedName"
|
||||
HeaderContentLength string = "Content-Length"
|
||||
HeaderContentType string = "Content-Type"
|
||||
HeaderWopiVersion string = "X-WOPI-ItemVersion"
|
||||
)
|
||||
|
||||
// HttpAdapter will adapt the responses from the connector to HTTP.
|
||||
@@ -50,10 +51,8 @@ func NewHttpAdapter(gwc gatewayv1beta1.GatewayAPIClient, cfg *config.Config) *Ht
|
||||
),
|
||||
}
|
||||
|
||||
// TODO: check if we can get rid of custom log parsing completely
|
||||
httpAdapter.locks = &locks.NoopLockParser{}
|
||||
if strings.ToLower(cfg.App.Name) == "microsoftofficeonline" {
|
||||
httpAdapter.locks = &locks.LegacyLockParser{}
|
||||
}
|
||||
return httpAdapter
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/mocks"
|
||||
@@ -133,12 +134,13 @@ var _ = Describe("HttpAdapter", func() {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
fc.On("Lock", mock.Anything, "abc123", "").Times(1).Return(connector.NewResponseWithLock(409, "zzz111"), nil)
|
||||
fc.On("Lock", mock.Anything, "abc123", "").Times(1).Return(connector.NewResponseLockConflict("zzz111", "Lock Conflict"), nil)
|
||||
|
||||
httpAdapter.Lock(w, req)
|
||||
resp := w.Result()
|
||||
Expect(resp.StatusCode).To(Equal(409))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111"))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiLockFailureReason)).To(Equal("Lock Conflict"))
|
||||
})
|
||||
|
||||
It("Success", func() {
|
||||
@@ -148,11 +150,18 @@ var _ = Describe("HttpAdapter", func() {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
fc.On("Lock", mock.Anything, "abc123", "").Times(1).Return(connector.NewResponse(200), nil)
|
||||
fc.On("Lock", mock.Anything, "abc123", "").Times(1).Return(
|
||||
connector.NewResponseWithVersionAndLock(
|
||||
200,
|
||||
&typesv1beta1.Timestamp{Seconds: uint64(1234), Nanos: uint32(567)},
|
||||
"abc123",
|
||||
), nil)
|
||||
|
||||
httpAdapter.Lock(w, req)
|
||||
resp := w.Result()
|
||||
Expect(resp.StatusCode).To(Equal(200))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("abc123"))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiVersion)).To(Equal("v1234567"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -195,12 +204,13 @@ var _ = Describe("HttpAdapter", func() {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
fc.On("Lock", mock.Anything, "abc123", "qwerty").Times(1).Return(connector.NewResponseWithLock(409, "zzz111"), nil)
|
||||
fc.On("Lock", mock.Anything, "abc123", "qwerty").Times(1).Return(connector.NewResponseLockConflict("zzz111", "Lock Conflict"), nil)
|
||||
|
||||
httpAdapter.Lock(w, req)
|
||||
resp := w.Result()
|
||||
Expect(resp.StatusCode).To(Equal(409))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111"))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiLockFailureReason)).To(Equal("Lock Conflict"))
|
||||
})
|
||||
|
||||
It("Success", func() {
|
||||
@@ -211,11 +221,18 @@ var _ = Describe("HttpAdapter", func() {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
fc.On("Lock", mock.Anything, "abc123", "qwerty").Times(1).Return(connector.NewResponse(200), nil)
|
||||
fc.On("Lock", mock.Anything, "abc123", "qwerty").Times(1).Return(
|
||||
connector.NewResponseWithVersionAndLock(
|
||||
200,
|
||||
&typesv1beta1.Timestamp{Seconds: uint64(1234), Nanos: uint32(567)},
|
||||
"abc123",
|
||||
), nil)
|
||||
|
||||
httpAdapter.Lock(w, req)
|
||||
resp := w.Result()
|
||||
Expect(resp.StatusCode).To(Equal(200))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("abc123"))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiVersion)).To(Equal("v1234567"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -256,12 +273,13 @@ var _ = Describe("HttpAdapter", func() {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
fc.On("RefreshLock", mock.Anything, "abc123").Times(1).Return(connector.NewResponseWithLock(409, "zzz111"), nil)
|
||||
fc.On("RefreshLock", mock.Anything, "abc123").Times(1).Return(connector.NewResponseLockConflict("zzz111", "Lock Conflict"), nil)
|
||||
|
||||
httpAdapter.RefreshLock(w, req)
|
||||
resp := w.Result()
|
||||
Expect(resp.StatusCode).To(Equal(409))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111"))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiLockFailureReason)).To(Equal("Lock Conflict"))
|
||||
})
|
||||
|
||||
It("Success", func() {
|
||||
@@ -271,11 +289,18 @@ var _ = Describe("HttpAdapter", func() {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
fc.On("RefreshLock", mock.Anything, "abc123").Times(1).Return(connector.NewResponse(200), nil)
|
||||
fc.On("RefreshLock", mock.Anything, "abc123").Times(1).Return(
|
||||
connector.NewResponseWithVersionAndLock(
|
||||
200,
|
||||
&typesv1beta1.Timestamp{Seconds: uint64(1234), Nanos: uint32(5678)},
|
||||
"abc123",
|
||||
), nil)
|
||||
|
||||
httpAdapter.RefreshLock(w, req)
|
||||
resp := w.Result()
|
||||
Expect(resp.StatusCode).To(Equal(200))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("abc123"))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiVersion)).To(Equal("v12345678"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -315,12 +340,13 @@ var _ = Describe("HttpAdapter", func() {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
fc.On("UnLock", mock.Anything, "abc123").Times(1).Return(connector.NewResponseWithLock(409, "zzz111"), nil)
|
||||
fc.On("UnLock", mock.Anything, "abc123").Times(1).Return(connector.NewResponseLockConflict("zzz111", "Lock Conflict"), nil)
|
||||
|
||||
httpAdapter.UnLock(w, req)
|
||||
resp := w.Result()
|
||||
Expect(resp.StatusCode).To(Equal(409))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111"))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiLockFailureReason)).To(Equal("Lock Conflict"))
|
||||
})
|
||||
|
||||
It("Success", func() {
|
||||
@@ -330,11 +356,13 @@ var _ = Describe("HttpAdapter", func() {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
fc.On("UnLock", mock.Anything, "abc123").Times(1).Return(connector.NewResponse(200), nil)
|
||||
fc.On("UnLock", mock.Anything, "abc123").Times(1).Return(
|
||||
connector.NewResponseWithVersion(&typesv1beta1.Timestamp{Seconds: uint64(1234), Nanos: uint32(567)}), nil)
|
||||
|
||||
httpAdapter.UnLock(w, req)
|
||||
resp := w.Result()
|
||||
Expect(resp.StatusCode).To(Equal(200))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiVersion)).To(Equal("v1234567"))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -458,12 +486,14 @@ var _ = Describe("HttpAdapter", func() {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
cc.On("PutFile", mock.Anything, mock.Anything, int64(len(contentBody)), "abc123").Times(1).Return(connector.NewResponseWithLock(409, "zzz111"), nil)
|
||||
cc.On("PutFile", mock.Anything, mock.Anything, int64(len(contentBody)), "abc123").Times(1).Return(
|
||||
connector.NewResponseLockConflict("zzz111", "Lock Conflict"), nil)
|
||||
|
||||
httpAdapter.PutFile(w, req)
|
||||
resp := w.Result()
|
||||
Expect(resp.StatusCode).To(Equal(409))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111"))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiLockFailureReason)).To(Equal("Lock Conflict"))
|
||||
})
|
||||
|
||||
It("Success", func() {
|
||||
@@ -473,11 +503,18 @@ var _ = Describe("HttpAdapter", func() {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
cc.On("PutFile", mock.Anything, mock.Anything, int64(len(contentBody)), "abc123").Times(1).Return(connector.NewResponse(200), nil)
|
||||
cc.On("PutFile", mock.Anything, mock.Anything, int64(len(contentBody)), "abc123").Times(1).Return(
|
||||
connector.NewResponseWithVersionAndLock(
|
||||
200,
|
||||
&typesv1beta1.Timestamp{Seconds: uint64(1234), Nanos: uint32(567)},
|
||||
"abc123",
|
||||
), nil)
|
||||
|
||||
httpAdapter.PutFile(w, req)
|
||||
resp := w.Result()
|
||||
Expect(resp.StatusCode).To(Equal(200))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("abc123"))
|
||||
Expect(resp.Header.Get(connector.HeaderWopiVersion)).To(Equal("v1234567"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,7 +7,6 @@ package locks
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LockParser is the interface that wraps the ParseLock method
|
||||
@@ -54,7 +53,7 @@ func (*NoopLockParser) ParseLock(id string) string {
|
||||
// If the JSON string is not in the expected format, the original lockID will be returned.
|
||||
func (*LegacyLockParser) ParseLock(id string) string {
|
||||
var decodedValues map[string]interface{}
|
||||
err := json.NewDecoder(strings.NewReader(id)).Decode(&decodedValues)
|
||||
err := json.Unmarshal([]byte(id), &decodedValues)
|
||||
if err != nil || len(decodedValues) == 0 {
|
||||
return id
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package middleware_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestMiddleware(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Middleware Suite")
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package middleware
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
@@ -27,7 +28,6 @@ func CollaborationTracingMiddleware(next http.Handler) http.Handler {
|
||||
wopiMethod := r.Header.Get("X-WOPI-Override")
|
||||
|
||||
wopiFile := wopiContext.FileReference
|
||||
wopiUser := wopiContext.User.GetId()
|
||||
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String("ocis.wopi.sessionid", r.Header.Get("X-WOPI-SessionId")),
|
||||
@@ -36,9 +36,14 @@ func CollaborationTracingMiddleware(next http.Handler) http.Handler {
|
||||
attribute.String("ocis.wopi.resource.id.opaque", wopiFile.GetResourceId().GetOpaqueId()),
|
||||
attribute.String("ocis.wopi.resource.id.space", wopiFile.GetResourceId().GetSpaceId()),
|
||||
attribute.String("ocis.wopi.resource.path", wopiFile.GetPath()),
|
||||
attribute.String("ocis.wopi.user.idp", wopiUser.GetIdp()),
|
||||
attribute.String("ocis.wopi.user.opaque", wopiUser.GetOpaqueId()),
|
||||
attribute.String("ocis.wopi.user.type", wopiUser.GetType().String()),
|
||||
}
|
||||
|
||||
if wopiUser, ok := ctxpkg.ContextGetUser(r.Context()); ok {
|
||||
attrs = append(attrs, []attribute.KeyValue{
|
||||
attribute.String("ocis.wopi.user.idp", wopiUser.GetId().GetIdp()),
|
||||
attribute.String("ocis.wopi.user.opaque", wopiUser.GetId().GetOpaqueId()),
|
||||
attribute.String("ocis.wopi.user.type", wopiUser.GetId().GetType().String()),
|
||||
}...)
|
||||
}
|
||||
span.SetAttributes(attrs...)
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
rjwt "github.com/cs3org/reva/v2/pkg/token/manager/jwt"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/helpers"
|
||||
@@ -29,7 +29,6 @@ type WopiContext struct {
|
||||
AccessToken string
|
||||
ViewOnlyToken string
|
||||
FileReference *providerv1beta1.Reference
|
||||
User *userv1beta1.User
|
||||
ViewMode appproviderv1beta1.ViewMode
|
||||
}
|
||||
|
||||
@@ -45,8 +44,6 @@ type WopiContext struct {
|
||||
// * A contextual zerologger containing information about the request
|
||||
// and the WopiContext
|
||||
func WopiContextAuthMiddleware(cfg *config.Config, next http.Handler) http.Handler {
|
||||
// compile a regexp here to extract the fileid from the URL
|
||||
fileIDregexp := regexp.MustCompile(`^/wopi/files/([0-9a-f]{64})(/.*)?$`)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
accessToken := r.URL.Query().Get("access_token")
|
||||
if accessToken == "" {
|
||||
@@ -76,11 +73,25 @@ func WopiContextAuthMiddleware(cfg *config.Config, next http.Handler) http.Handl
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
tokenManager, err := rjwt.New(map[string]interface{}{
|
||||
"secret": cfg.TokenManager.JWTSecret,
|
||||
"expires": int64(24 * 60 * 60),
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
user, _, err := tokenManager.DismantleToken(ctx, wopiContextAccessToken)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
claims.WopiContext.AccessToken = wopiContextAccessToken
|
||||
|
||||
ctx = context.WithValue(ctx, wopiContextKey, claims.WopiContext)
|
||||
// authentication for the CS3 api
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, claims.WopiContext.AccessToken)
|
||||
ctx = ctxpkg.ContextSetUser(ctx, user)
|
||||
|
||||
// include additional info in the context's logger
|
||||
// we might need to check https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/common-headers
|
||||
@@ -94,13 +105,13 @@ func WopiContextAuthMiddleware(cfg *config.Config, next http.Handler) http.Handl
|
||||
Str("WopiStamp", r.Header.Get("X-WOPI-TimeStamp")).
|
||||
Str("FileReference", claims.WopiContext.FileReference.String()).
|
||||
Str("ViewMode", claims.WopiContext.ViewMode.String()).
|
||||
Str("Requester", claims.WopiContext.User.GetId().String()).
|
||||
Str("Requester", user.GetId().String()).
|
||||
Logger()
|
||||
ctx = wopiLogger.WithContext(ctx)
|
||||
|
||||
hashedRef := helpers.HashResourceId(claims.WopiContext.FileReference.GetResourceId())
|
||||
matches := fileIDregexp.FindStringSubmatch(r.URL.Path)
|
||||
if len(matches) < 2 || matches[1] != hashedRef {
|
||||
fileID := parseWopiFileID(cfg, r.URL.Path)
|
||||
if fileID != hashedRef {
|
||||
wopiLogger.Error().Msg("file reference in the URL doesn't match the one inside the access token")
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
@@ -157,3 +168,36 @@ func GenerateWopiToken(wopiContext WopiContext, cfg *config.Config) (string, int
|
||||
|
||||
return accessToken, claims.ExpiresAt.UnixMilli(), err
|
||||
}
|
||||
|
||||
// parseWopiFileID extracts the file id from a wopi path
|
||||
//
|
||||
// If the file id is a jwt, it will be decoded and the file id will be extracted from the jwt claims.
|
||||
// If the file id is not a jwt, it will be returned as is.
|
||||
func parseWopiFileID(cfg *config.Config, path string) string {
|
||||
s := strings.Split(path, "/")
|
||||
if len(s) < 4 || (s[1] != "wopi" && s[2] != "files") {
|
||||
return path
|
||||
}
|
||||
// check if the fileid is a jwt
|
||||
if strings.Contains(s[3], ".") {
|
||||
token, err := jwt.Parse(s[3], func(_ *jwt.Token) (interface{}, error) {
|
||||
return []byte(cfg.Wopi.ProxySecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return s[3]
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return s[3]
|
||||
}
|
||||
|
||||
f, ok := claims["f"].(string)
|
||||
if !ok {
|
||||
return s[3]
|
||||
}
|
||||
return f
|
||||
}
|
||||
// fileid is not a jwt
|
||||
return s[3]
|
||||
}
|
||||
|
||||
226
services/collaboration/pkg/middleware/wopicontext_test.go
Normal file
226
services/collaboration/pkg/middleware/wopicontext_test.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package middleware_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/token"
|
||||
rjwt "github.com/cs3org/reva/v2/pkg/token/manager/jwt"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/helpers"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/middleware"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/wopisrc"
|
||||
)
|
||||
|
||||
var _ = Describe("Wopi Context Middleware", func() {
|
||||
var (
|
||||
cfg *config.Config
|
||||
ctx context.Context
|
||||
mw http.Handler
|
||||
rid *providerv1beta1.ResourceId
|
||||
tknMngr token.Manager
|
||||
user *userv1beta1.User
|
||||
src *url.URL
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
var err error
|
||||
cfg = &config.Config{
|
||||
TokenManager: &config.TokenManager{JWTSecret: "jwtSecret"},
|
||||
Wopi: config.Wopi{
|
||||
Secret: "wopiSecret",
|
||||
WopiSrc: "https://localhost:9300",
|
||||
},
|
||||
}
|
||||
|
||||
ctx = context.Background()
|
||||
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
mw = middleware.WopiContextAuthMiddleware(cfg, next)
|
||||
|
||||
tknMngr, err = rjwt.New(map[string]interface{}{
|
||||
"secret": cfg.TokenManager.JWTSecret,
|
||||
"expires": int64(24 * 60 * 60),
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
user = &userv1beta1.User{
|
||||
Id: &userv1beta1.UserId{
|
||||
Idp: "example.com",
|
||||
OpaqueId: "12345",
|
||||
Type: userv1beta1.UserType_USER_TYPE_PRIMARY,
|
||||
},
|
||||
Username: "admin",
|
||||
Mail: "admin@example.com",
|
||||
}
|
||||
|
||||
rid = &providerv1beta1.ResourceId{
|
||||
StorageId: "storageID",
|
||||
OpaqueId: "opaqueID",
|
||||
SpaceId: "spaceID",
|
||||
}
|
||||
|
||||
src, err = url.Parse(cfg.Wopi.WopiSrc)
|
||||
src.Path = path.Join("wopi", "files", helpers.HashResourceId(rid))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
It("Should not authorize with empty access token", func() {
|
||||
req := httptest.NewRequest("GET", src.String(), nil).WithContext(ctx)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
mw.ServeHTTP(resp, req)
|
||||
Expect(resp.Code).To(Equal(http.StatusUnauthorized))
|
||||
})
|
||||
It("Should not authorize with malformed access token", func() {
|
||||
req := httptest.NewRequest("GET", src.String(), nil).WithContext(ctx)
|
||||
q := req.URL.Query()
|
||||
q.Add("access_token", "token")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
mw.ServeHTTP(resp, req)
|
||||
Expect(resp.Code).To(Equal(http.StatusUnauthorized))
|
||||
})
|
||||
It("Should not authorize when fileID mismatches", func() {
|
||||
req := httptest.NewRequest("GET", src.String(), nil).WithContext(ctx)
|
||||
// create request with different fileID in the wopi context
|
||||
token, err := tknMngr.MintToken(ctx, user, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
wopiContext := middleware.WopiContext{
|
||||
AccessToken: token,
|
||||
ViewMode: appprovider.ViewMode_VIEW_MODE_READ_WRITE,
|
||||
FileReference: &providerv1beta1.Reference{
|
||||
ResourceId: &providerv1beta1.ResourceId{
|
||||
StorageId: "storageID",
|
||||
OpaqueId: "opaqueID2",
|
||||
SpaceId: "spaceID",
|
||||
},
|
||||
Path: ".",
|
||||
},
|
||||
}
|
||||
wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg)
|
||||
q := req.URL.Query()
|
||||
q.Add("access_token", wopiToken)
|
||||
q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
mw.ServeHTTP(resp, req)
|
||||
Expect(resp.Code).To(Equal(http.StatusUnauthorized))
|
||||
})
|
||||
It("Should not authorize with wrong wopi secret", func() {
|
||||
src.Path = path.Join("wopi", "files", helpers.HashResourceId(rid))
|
||||
req := httptest.NewRequest("GET", src.String(), nil).WithContext(ctx)
|
||||
token, err := tknMngr.MintToken(ctx, user, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
wopiContext := middleware.WopiContext{
|
||||
AccessToken: token,
|
||||
}
|
||||
// use wrong wopi secret when generating the wopi token
|
||||
wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, &config.Config{Wopi: config.Wopi{
|
||||
Secret: "wrongSecret",
|
||||
}})
|
||||
q := req.URL.Query()
|
||||
q.Add("access_token", wopiToken)
|
||||
q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
mw.ServeHTTP(resp, req)
|
||||
Expect(resp.Code).To(Equal(http.StatusUnauthorized))
|
||||
})
|
||||
It("Should authorize successful", func() {
|
||||
req := httptest.NewRequest("GET", src.String(), nil).WithContext(ctx)
|
||||
token, err := tknMngr.MintToken(ctx, user, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
wopiContext := middleware.WopiContext{
|
||||
AccessToken: token,
|
||||
ViewMode: appprovider.ViewMode_VIEW_MODE_READ_WRITE,
|
||||
FileReference: &providerv1beta1.Reference{
|
||||
ResourceId: rid,
|
||||
Path: ".",
|
||||
},
|
||||
}
|
||||
wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg)
|
||||
q := req.URL.Query()
|
||||
q.Add("access_token", wopiToken)
|
||||
q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
mw.ServeHTTP(resp, req)
|
||||
Expect(resp.Code).To(Equal(http.StatusOK))
|
||||
})
|
||||
It("Should not authorize with proxy when fileID mismatches", func() {
|
||||
cfg.Wopi.ProxySecret = "proxySecret"
|
||||
cfg.Wopi.ProxyURL = "https://proxy"
|
||||
src, err := wopisrc.GenerateWopiSrc(helpers.HashResourceId(rid), cfg)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
req := httptest.NewRequest("GET", src.String(), nil).WithContext(ctx)
|
||||
token, err := tknMngr.MintToken(ctx, user, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
wopiContext := middleware.WopiContext{
|
||||
AccessToken: token,
|
||||
ViewMode: appprovider.ViewMode_VIEW_MODE_READ_WRITE,
|
||||
FileReference: &providerv1beta1.Reference{
|
||||
ResourceId: &providerv1beta1.ResourceId{
|
||||
StorageId: "storageID",
|
||||
OpaqueId: "opaqueID3",
|
||||
SpaceId: "spaceID",
|
||||
},
|
||||
Path: ".",
|
||||
},
|
||||
}
|
||||
wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg)
|
||||
q := req.URL.Query()
|
||||
q.Add("access_token", wopiToken)
|
||||
q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
mw.ServeHTTP(resp, req)
|
||||
Expect(resp.Code).To(Equal(http.StatusUnauthorized))
|
||||
})
|
||||
It("Should authorize successful with proxy", func() {
|
||||
cfg.Wopi.ProxySecret = "proxySecret"
|
||||
cfg.Wopi.ProxyURL = "https://proxy"
|
||||
src, err := wopisrc.GenerateWopiSrc(helpers.HashResourceId(rid), cfg)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
req := httptest.NewRequest("GET", src.String(), nil).WithContext(ctx)
|
||||
token, err := tknMngr.MintToken(ctx, user, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
wopiContext := middleware.WopiContext{
|
||||
AccessToken: token,
|
||||
ViewMode: appprovider.ViewMode_VIEW_MODE_READ_WRITE,
|
||||
FileReference: &providerv1beta1.Reference{
|
||||
ResourceId: rid,
|
||||
Path: ".",
|
||||
},
|
||||
}
|
||||
wopiToken, ttl, err := middleware.GenerateWopiToken(wopiContext, cfg)
|
||||
q := req.URL.Query()
|
||||
q.Add("access_token", wopiToken)
|
||||
q.Add("access_token_ttl", strconv.FormatInt(ttl, 10))
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
resp := httptest.NewRecorder()
|
||||
mw.ServeHTTP(resp, req)
|
||||
Expect(resp.Code).To(Equal(http.StatusOK))
|
||||
})
|
||||
})
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/cs3org/reva/v2/pkg/utils"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/wopisrc"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
@@ -109,7 +110,6 @@ func (s *Service) OpenInApp(
|
||||
AccessToken: req.GetAccessToken(), // it will be encrypted
|
||||
ViewOnlyToken: utils.ReadPlainFromOpaque(req.GetOpaque(), "viewOnlyToken"),
|
||||
FileReference: &providerFileRef,
|
||||
User: user,
|
||||
ViewMode: req.GetViewMode(),
|
||||
}
|
||||
|
||||
@@ -201,11 +201,10 @@ func (s *Service) addQueryToURL(baseURL string, req *appproviderv1beta1.OpenInAp
|
||||
// so that all sessions on one file end on the same office server
|
||||
fileRef := helpers.HashResourceId(req.GetResourceInfo().GetId())
|
||||
|
||||
wopiSrcURL, err := url.Parse(s.config.Wopi.WopiSrc)
|
||||
wopiSrcURL, err := wopisrc.GenerateWopiSrc(fileRef, s.config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
wopiSrcURL.Path = path.Join("wopi", "files", fileRef)
|
||||
|
||||
q := u.Query()
|
||||
q.Add("WOPISrc", wopiSrcURL.String())
|
||||
@@ -216,6 +215,38 @@ func (s *Service) addQueryToURL(baseURL string, req *appproviderv1beta1.OpenInAp
|
||||
|
||||
lang := utils.ReadPlainFromOpaque(req.GetOpaque(), "lang")
|
||||
|
||||
// @TODO: this is a temporary solution until we figure out how to send these from oc web
|
||||
switch lang {
|
||||
case "bg":
|
||||
lang = "bg-BG"
|
||||
case "cs":
|
||||
lang = "cs-CZ"
|
||||
case "de":
|
||||
lang = "de-DE"
|
||||
case "en":
|
||||
lang = "en-US"
|
||||
case "es":
|
||||
lang = "es-ES"
|
||||
case "fr":
|
||||
lang = "fr-FR"
|
||||
case "gl":
|
||||
lang = "gl-ES"
|
||||
case "it":
|
||||
lang = "it-IT"
|
||||
case "nl":
|
||||
lang = "nl-NL"
|
||||
case "ko":
|
||||
lang = "ko-KR"
|
||||
case "sq":
|
||||
lang = "sq-AL"
|
||||
case "sv":
|
||||
lang = "sv-SE"
|
||||
case "tr":
|
||||
lang = "tr-TR"
|
||||
case "zh":
|
||||
lang = "zh-CN"
|
||||
}
|
||||
|
||||
if lang != "" {
|
||||
switch strings.ToLower(s.config.App.Name) {
|
||||
case "collabora":
|
||||
|
||||
@@ -187,15 +187,60 @@ var _ = Describe("Discovery", func() {
|
||||
Entry("Microsoft chat no lang", "Microsoft", "", false, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
|
||||
Entry("Collabora chat no lang", "Collabora", "", false, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
|
||||
Entry("OnlyOffice chat no lang", "OnlyOffice", "", false, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
|
||||
Entry("Microsoft chat lang", "Microsoft", "de", false, "https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=de&WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
|
||||
Entry("Collabora chat lang", "Collabora", "de", false, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&lang=de"),
|
||||
Entry("OnlyOffice chat lang", "OnlyOffice", "de", false, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&ui=de"),
|
||||
Entry("Microsoft chat lang", "Microsoft", "de", false, "https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=de-DE&WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
|
||||
Entry("Collabora chat lang", "Collabora", "de", false, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&lang=de-DE"),
|
||||
Entry("OnlyOffice chat lang", "OnlyOffice", "de", false, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&ui=de-DE"),
|
||||
Entry("Microsoft no chat no lang", "Microsoft", "", true, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
|
||||
Entry("Collabora no chat no lang", "Collabora", "", true, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
|
||||
Entry("OnlyOffice no chat no lang", "OnlyOffice", "", true, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
|
||||
Entry("Microsoft no chat lang", "Microsoft", "de", true, "https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=de&WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
|
||||
Entry("Collabora no chat lang", "Collabora", "de", true, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1&lang=de"),
|
||||
Entry("OnlyOffice no chat lang", "OnlyOffice", "de", true, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1&ui=de"),
|
||||
Entry("Microsoft no chat lang", "Microsoft", "de", true, "https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=de-DE&WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
|
||||
Entry("Collabora no chat lang", "Collabora", "de", true, "https://test.server.prv/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1&lang=de-DE"),
|
||||
Entry("OnlyOffice no chat lang", "OnlyOffice", "de", true, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1&ui=de-DE"),
|
||||
)
|
||||
It("Success with Wopi Proxy", func() {
|
||||
ctx := context.Background()
|
||||
nowTime := time.Now()
|
||||
|
||||
cfg.Wopi.WopiSrc = "https://wopiserver.test.prv"
|
||||
cfg.Wopi.Secret = "my_supa_secret"
|
||||
cfg.Wopi.ProxyURL = "https://office.proxy.test.prv"
|
||||
cfg.Wopi.ProxySecret = "your_supa_secret"
|
||||
cfg.App.Name = "Microsoft"
|
||||
|
||||
myself := &userv1beta1.User{
|
||||
Id: &userv1beta1.UserId{
|
||||
Idp: "myIdp",
|
||||
OpaqueId: "opaque001",
|
||||
Type: userv1beta1.UserType_USER_TYPE_PRIMARY,
|
||||
},
|
||||
Username: "username",
|
||||
}
|
||||
|
||||
req := &appproviderv1beta1.OpenInAppRequest{
|
||||
ResourceInfo: &providerv1beta1.ResourceInfo{
|
||||
Id: &providerv1beta1.ResourceId{
|
||||
StorageId: "myStorage",
|
||||
OpaqueId: "storageOpaque001",
|
||||
SpaceId: "SpaceA",
|
||||
},
|
||||
Path: "/path/to/file.docx",
|
||||
},
|
||||
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
|
||||
AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime),
|
||||
}
|
||||
req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "lang", "en")
|
||||
|
||||
gatewayClient.On("WhoAmI", mock.Anything, mock.Anything).Times(1).Return(&gatewayv1beta1.WhoAmIResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
User: myself,
|
||||
}, nil)
|
||||
|
||||
resp, err := srv.OpenInApp(ctx, req)
|
||||
Expect(err).To(Succeed())
|
||||
Expect(resp.GetStatus().GetCode()).To(Equal(rpcv1beta1.Code_CODE_OK))
|
||||
Expect(resp.GetAppUrl().GetMethod()).To(Equal("POST"))
|
||||
Expect(resp.GetAppUrl().GetAppUrl()).To(Equal("https://test.server.prv/hosting/wopi/word/edit?UI_LLCC=en-US&WOPISrc=https%3A%2F%2Foffice.proxy.test.prv%2Fwopi%2Ffiles%2FeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1IjoiaHR0cHM6Ly93b3Bpc2VydmVyLnRlc3QucHJ2L3dvcGkvZmlsZXMvIiwiZiI6IjJmNmVjMTg2OTZkZDEwMDgxMDY3NDliZDk0MTA2ZTVjZmFkNWMwOWUxNWRlN2I3NzA4OGQwMzg0M2U3MWI0M2UifQ.yfyLHZ18Z1MFOa6u7AP0LqfIiQ9X5AMkYauEZGhbCNs"))
|
||||
Expect(resp.GetAppUrl().GetFormParameters()["access_token_ttl"]).To(Equal(strconv.FormatInt(nowTime.Add(5*time.Hour).Unix()*1000, 10)))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
72
services/collaboration/pkg/wopisrc/wopisrc.go
Normal file
72
services/collaboration/pkg/wopisrc/wopisrc.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package wopisrc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
)
|
||||
|
||||
// GenerateWopiSrc generates a WOPI src URL for the given file reference.
|
||||
// If a proxy URL and proxy secret are configured, the URL will be generated
|
||||
// as a jwt token that is signed with the proxy secret and contains the file reference
|
||||
// and the WOPI src URL.
|
||||
// Example:
|
||||
// https://cloud.proxy.com/wopi/files/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1IjoiaHR0cHM6Ly9vY2lzLnRlYW0vd29waS9maWxlcy8iLCJmIjoiMTIzNDU2In0.6ol9PQXGKktKfAri8tsJ4X_a9rIeosJ7id6KTQW6Ui0
|
||||
//
|
||||
// If no proxy URL and proxy secret are configured, the URL will be generated
|
||||
// as a direct URL that contains the file reference.
|
||||
// Example:
|
||||
// https:/ocis.team/wopi/files/12312678470610632091729803710923
|
||||
func GenerateWopiSrc(fileRef string, cfg *config.Config) (*url.URL, error) {
|
||||
wopiSrcURL, err := url.Parse(cfg.Wopi.WopiSrc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wopiSrcURL.Host == "" {
|
||||
return nil, errors.New("invalid WopiSrc URL")
|
||||
}
|
||||
|
||||
if cfg.Wopi.ProxyURL != "" && cfg.Wopi.ProxySecret != "" {
|
||||
return generateProxySrc(fileRef, cfg.Wopi.ProxyURL, cfg.Wopi.ProxySecret, wopiSrcURL)
|
||||
}
|
||||
|
||||
return generateDirectSrc(fileRef, wopiSrcURL)
|
||||
}
|
||||
|
||||
func generateDirectSrc(fileRef string, wopiSrcURL *url.URL) (*url.URL, error) {
|
||||
wopiSrcURL.Path = path.Join("wopi", "files", fileRef)
|
||||
return wopiSrcURL, nil
|
||||
}
|
||||
|
||||
func generateProxySrc(fileRef string, proxyUrl string, proxySecret string, wopiSrcURL *url.URL) (*url.URL, error) {
|
||||
proxyURL, err := url.Parse(proxyUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if proxyURL.Host == "" {
|
||||
return nil, errors.New("invalid proxy URL")
|
||||
}
|
||||
|
||||
wopiSrcURL.Path = path.Join("wopi", "files")
|
||||
|
||||
type tokenClaims struct {
|
||||
URL string `json:"u"`
|
||||
FileID string `json:"f"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, tokenClaims{
|
||||
FileID: fileRef,
|
||||
// the string value from the URL package always ends with a slash
|
||||
// the office365 proxy assumes that we have a trailing slash
|
||||
URL: wopiSrcURL.String() + "/",
|
||||
})
|
||||
tokenString, err := token.SignedString([]byte(proxySecret))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proxyURL.Path = path.Join("wopi", "files", tokenString)
|
||||
return proxyURL, nil
|
||||
}
|
||||
13
services/collaboration/pkg/wopisrc/wopisrc_suite_test.go
Normal file
13
services/collaboration/pkg/wopisrc/wopisrc_suite_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package wopisrc_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestWopisrc(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Wopisrc Suite")
|
||||
}
|
||||
64
services/collaboration/pkg/wopisrc/wopisrc_test.go
Normal file
64
services/collaboration/pkg/wopisrc/wopisrc_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package wopisrc_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/wopisrc"
|
||||
)
|
||||
|
||||
var _ = Describe("Wopisrc Test", func() {
|
||||
var (
|
||||
c *config.Config
|
||||
)
|
||||
|
||||
Context("GenerateWopiSrc", func() {
|
||||
BeforeEach(func() {
|
||||
c = &config.Config{
|
||||
Wopi: config.Wopi{
|
||||
WopiSrc: "https://ocis.team/wopi/files",
|
||||
ProxyURL: "https://cloud.proxy.com",
|
||||
ProxySecret: "secret",
|
||||
},
|
||||
}
|
||||
})
|
||||
When("WopiSrc URL is incorrect", func() {
|
||||
c = &config.Config{
|
||||
Wopi: config.Wopi{
|
||||
WopiSrc: "https:&//ocis.team/wopi/files",
|
||||
},
|
||||
}
|
||||
url, err := wopisrc.GenerateWopiSrc("123456", c)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(url).To(BeNil())
|
||||
})
|
||||
When("proxy URL is incorrect", func() {
|
||||
c = &config.Config{
|
||||
Wopi: config.Wopi{
|
||||
WopiSrc: "https://ocis.team/wopi/files",
|
||||
ProxyURL: "cloud",
|
||||
ProxySecret: "secret",
|
||||
},
|
||||
}
|
||||
url, err := wopisrc.GenerateWopiSrc("123456", c)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(url).To(BeNil())
|
||||
})
|
||||
When("proxy URL and proxy secret are configured", func() {
|
||||
It("should generate a WOPI src URL as a jwt token", func() {
|
||||
url, err := wopisrc.GenerateWopiSrc("123456", c)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(url.String()).To(Equal("https://cloud.proxy.com/wopi/files/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1IjoiaHR0cHM6Ly9vY2lzLnRlYW0vd29waS9maWxlcy8iLCJmIjoiMTIzNDU2In0.6ol9PQXGKktKfAri8tsJ4X_a9rIeosJ7id6KTQW6Ui0"))
|
||||
})
|
||||
})
|
||||
When("proxy URL and proxy secret are not configured", func() {
|
||||
It("should generate a WOPI src URL as a direct URL", func() {
|
||||
c.Wopi.ProxyURL = ""
|
||||
c.Wopi.ProxySecret = ""
|
||||
url, err := wopisrc.GenerateWopiSrc("123456", c)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(url.String()).To(Equal("https://ocis.team/wopi/files/123456"))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user