feat: office 365 proxy support

This commit is contained in:
Michael Barz
2024-07-24 23:02:46 +02:00
parent 09ffe3891e
commit bfa3dd07bc
24 changed files with 1153 additions and 218 deletions

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" introductionVersion:"%%NEXT%%"`
}
type ProofKeys struct {

View File

@@ -5,4 +5,6 @@ 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%%"`
ProxyURL string `yaml:"proxy_url" env:"COLLABORATION_WOPI_PROXY_URL" desc:"The URL to the ownCloud Office365 WOPI proxy." introductionVersion:"%%NEXT%%"`
ProxySecret string `yaml:"proxy_secret" env:"COLLABORATION_WOPI_PROXY_SECRET" desc:"The secret to authenticate against the ownCloud Office365 WOPI proxy." introductionVersion:"%%NEXT%%"`
}

View File

@@ -1,5 +1,10 @@
package connector
import (
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/helpers"
)
// 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 +38,61 @@ 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(status int, mtime *types.Timestamp) *ConnectorResponse {
return &ConnectorResponse{
Status: status,
Headers: map[string]string{
HeaderWopiVersion: helpers.GetVersion(mtime),
},
}
}
// NewResponseConflictWithVersion creates a new ConnectorResponse with the status 409
// and the "X-WOPI-ItemVersion" header having the value in the mtime parameter.
// The lockFailureReason parameter will be included in the "X-WOPI-LockFailureReason".
func NewResponseConflictWithVersion(mtime *types.Timestamp, lockFailureReason string) *ConnectorResponse {
return &ConnectorResponse{
Status: 409,
Headers: map[string]string{
HeaderWopiVersion: helpers.GetVersion(mtime),
HeaderWopiLockFailureReason: lockFailureReason,
},
}
}
// 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: helpers.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.
//

View File

@@ -17,6 +17,7 @@ import (
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/owncloud/ocis/v2/ocis-pkg/tracing"
"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/rs/zerolog"
"go.opentelemetry.io/otel/propagation"
@@ -29,7 +30,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 +62,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 +77,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 +181,14 @@ func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error
return NewConnectorError(500, "GetFile: Downloading the file failed")
}
helpers.SetVersionHeader(w, 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 +213,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 +246,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 +263,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 +384,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(200, 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(200, 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 NewResponseWithVersionAndLock(200, statResp.GetInfo().GetMtime(), resp.GetLock().GetLockId()), 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(200, 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 NewResponseConflictWithVersion(statResp.GetInfo().GetMtime(), "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(200, 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: helpers.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,20 @@ 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(2))
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("abcdef123"))
Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v123456789"))
})
It("Set lock mismatches but get lock doesn't return lockId", func() {
@@ -256,8 +271,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 +287,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 +303,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 +325,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 +351,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 +378,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 +402,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 +426,20 @@ 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(2))
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("abcdef123"))
Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v123456789"))
})
It("Refresh lock mismatches but get lock doesn't return lockId", func() {
@@ -404,8 +453,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 +469,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 +485,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 +499,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 +509,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 +535,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 +557,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 +578,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 +598,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 +618,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 +642,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 +658,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 +671,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 +681,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 +707,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 +729,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 +755,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 +775,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 +795,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 +819,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 +835,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 +878,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 +932,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 +988,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 +1057,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 +1091,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 +1131,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 +1184,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 +1243,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 +1278,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 +1342,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 +1359,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 +1380,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 +1397,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 +1410,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 +1446,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 +1494,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 +1518,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 +1561,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 +1593,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 +1631,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 +1660,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 +1687,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 +1712,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 +1727,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 +1767,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 +1777,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 +1801,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 +1820,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 +1833,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.
@@ -51,9 +52,6 @@ func NewHttpAdapter(gwc gatewayv1beta1.GatewayAPIClient, cfg *config.Config) *Ht
}
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,15 @@ 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(200,
&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 +488,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 +505,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

@@ -0,0 +1,41 @@
package helpers
import (
"strings"
"github.com/golang-jwt/jwt/v5"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
)
// 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,20 @@
package helpers
import (
"net/http"
"strconv"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
)
// SetVersionHeader sets a WOPI version header on the response writer
func SetVersionHeader(w http.ResponseWriter, t *typesv1beta1.Timestamp) {
// non-canonical headers can only be set directly on the header map
w.Header().Set("X-WOPI-ItemVersion", GetVersion(t))
}
// GetVersion returns a string representation of the timestamp
func GetVersion(timestamp *typesv1beta1.Timestamp) string {
return "v" + strconv.FormatUint(timestamp.GetSeconds(), 10) +
strconv.FormatUint(uint64(timestamp.GetNanos()), 10)
}

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,11 @@ import (
"errors"
"fmt"
"net/http"
"regexp"
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 +28,6 @@ type WopiContext struct {
AccessToken string
ViewOnlyToken string
FileReference *providerv1beta1.Reference
User *userv1beta1.User
ViewMode appproviderv1beta1.ViewMode
}
@@ -45,8 +43,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 +72,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 +104,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 := helpers.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

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

@@ -121,6 +121,7 @@ func prepareRoutes(r *chi.Mux, options Options) {
// authentication and wopi context
return colabmiddleware.WopiContextAuthMiddleware(options.Config, h)
},
colabmiddleware.CollaborationTracingMiddleware,
)

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