Merge pull request #9686 from owncloud/ms365

feat: Microsoft office 365 Cloud and Office Online Server support
This commit is contained in:
Michael Barz
2024-08-30 19:38:35 +02:00
committed by GitHub
23 changed files with 1127 additions and 219 deletions

View 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

View File

@@ -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`.

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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%%"`
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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"))
})
})
})

View File

@@ -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
}

View File

@@ -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))
})

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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"))
})
})
})

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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...)

View File

@@ -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]
}

View 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))
})
})

View File

@@ -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":

View File

@@ -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)))
})
})
})

View 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
}

View 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")
}

View 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"))
})
})
})
})