feat: office 365 proxy support

This commit is contained in:
Michael Barz
2024-08-30 16:20:04 +02:00
parent 09ffe3891e
commit bfa3dd07bc
24 changed files with 1153 additions and 218 deletions
@@ -7,6 +7,8 @@ import (
connector "github.com/owncloud/ocis/v2/services/collaboration/pkg/connector" connector "github.com/owncloud/ocis/v2/services/collaboration/pkg/connector"
http "net/http"
io "io" io "io"
mock "github.com/stretchr/testify/mock" mock "github.com/stretchr/testify/mock"
@@ -25,17 +27,17 @@ func (_m *ContentConnectorService) EXPECT() *ContentConnectorService_Expecter {
return &ContentConnectorService_Expecter{mock: &_m.Mock} return &ContentConnectorService_Expecter{mock: &_m.Mock}
} }
// GetFile provides a mock function with given fields: ctx, writer // GetFile provides a mock function with given fields: ctx, w
func (_m *ContentConnectorService) GetFile(ctx context.Context, writer io.Writer) error { func (_m *ContentConnectorService) GetFile(ctx context.Context, w http.ResponseWriter) error {
ret := _m.Called(ctx, writer) ret := _m.Called(ctx, w)
if len(ret) == 0 { if len(ret) == 0 {
panic("no return value specified for GetFile") panic("no return value specified for GetFile")
} }
var r0 error var r0 error
if rf, ok := ret.Get(0).(func(context.Context, io.Writer) error); ok { if rf, ok := ret.Get(0).(func(context.Context, http.ResponseWriter) error); ok {
r0 = rf(ctx, writer) r0 = rf(ctx, w)
} else { } else {
r0 = ret.Error(0) r0 = ret.Error(0)
} }
@@ -50,14 +52,14 @@ type ContentConnectorService_GetFile_Call struct {
// GetFile is a helper method to define mock.On call // GetFile is a helper method to define mock.On call
// - ctx context.Context // - ctx context.Context
// - writer io.Writer // - w http.ResponseWriter
func (_e *ContentConnectorService_Expecter) GetFile(ctx interface{}, writer interface{}) *ContentConnectorService_GetFile_Call { func (_e *ContentConnectorService_Expecter) GetFile(ctx interface{}, w interface{}) *ContentConnectorService_GetFile_Call {
return &ContentConnectorService_GetFile_Call{Call: _e.mock.On("GetFile", ctx, writer)} 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) { _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 return _c
} }
@@ -67,7 +69,7 @@ func (_c *ContentConnectorService_GetFile_Call) Return(_a0 error) *ContentConnec
return _c 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) _c.Call.Return(run)
return _c return _c
} }
+2 -1
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"` 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"` 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 { type ProofKeys struct {
@@ -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"` 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"` Secret string `yaml:"secret" env:"COLLABORATION_WOPI_SECRET" desc:"Used to mint and verify WOPI JWT tokens and encrypt and decrypt the REVA JWT token embedded in the WOPI JWT token." introductionVersion:"6.0.0"`
DisableChat bool `yaml:"disable_chat" env:"COLLABORATION_WOPI_DISABLE_CHAT;OCIS_WOPI_DISABLE_CHAT" desc:"Disable chat in the frontend." introductionVersion:"%%NEXT%%"` DisableChat bool `yaml:"disable_chat" env:"COLLABORATION_WOPI_DISABLE_CHAT;OCIS_WOPI_DISABLE_CHAT" desc:"Disable chat in the frontend." 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%%"`
} }
@@ -1,5 +1,10 @@
package connector 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. // ConnectorResponse represent a response from the FileConnectorService.
// The ConnectorResponse is oriented to HTTP, so it has the Status, Headers // The ConnectorResponse is oriented to HTTP, so it has the Status, Headers
// and Body that the actual HTTP response should have. This includes HTTP // 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 // NewResponseSuccessBody creates a new ConnectorResponse with a fixed 200
// (success) status and the specified body. The headers will be nil. // (success) status and the specified body. The headers will be nil.
// //
@@ -17,6 +17,7 @@ import (
revactx "github.com/cs3org/reva/v2/pkg/ctx" revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/owncloud/ocis/v2/ocis-pkg/tracing" "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/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/middleware"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/propagation"
@@ -29,7 +30,7 @@ import (
// Target file is within the WOPI context // Target file is within the WOPI context
type ContentConnectorService interface { type ContentConnectorService interface {
// GetFile downloads the file and write its contents in the provider writer // 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 // PutFile uploads the stream up to the stream length. The file should be
// locked beforehand, so the lockID needs to be provided. // locked beforehand, so the lockID needs to be provided.
// The current lockID will be returned ONLY if a conflict happens (the file is // 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 // You can pass a pre-configured zerologger instance through the context that
// will be used to log messages. // 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. // 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) wopiContext, err := middleware.WopiContextFromCtx(ctx)
if err != nil { if err != nil {
return err return err
@@ -74,6 +77,16 @@ func (c *ContentConnector) GetFile(ctx context.Context, writer io.Writer) error
Logger() Logger()
logger.Debug().Msg("GetFile: start") 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 // Initiate download request
req := &providerv1beta1.InitiateFileDownloadRequest{ req := &providerv1beta1.InitiateFileDownloadRequest{
Ref: wopiContext.FileReference, 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") return NewConnectorError(500, "GetFile: Downloading the file failed")
} }
helpers.SetVersionHeader(w, sResp.GetInfo().GetMtime())
// Copy the download into the writer // Copy the download into the writer
_, err = io.Copy(writer, httpResp.Body) _, err = io.Copy(w, httpResp.Body)
if err != nil { if err != nil {
logger.Error().Msg("GetFile: copying the file content to the response body failed") logger.Error().Msg("GetFile: copying the file content to the response body failed")
return err return err
} }
logger.Debug().Msg("GetFile: success") logger.Debug().Msg("GetFile: success")
return nil 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 // 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 // cases or if the method is successful, an empty string will be returned
// (check for err != nil to know if something went wrong) // (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) { func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, streamLength int64, lockID string) (*ConnectorResponse, error) {
wopiContext, err := middleware.WopiContextFromCtx(ctx) wopiContext, err := middleware.WopiContextFromCtx(ctx)
if err != nil { if err != nil {
@@ -230,13 +246,14 @@ func (c *ContentConnector) PutFile(ctx context.Context, stream io.Reader, stream
return NewResponse(500), nil return NewResponse(500), nil
} }
mtime := statRes.GetInfo().GetMtime()
// If there is a lock and it mismatches, return 409 // If there is a lock and it mismatches, return 409
if statRes.GetInfo().GetLock() != nil && statRes.GetInfo().GetLock().GetLockId() != lockID { if statRes.GetInfo().GetLock() != nil && statRes.GetInfo().GetLock().GetLockId() != lockID {
logger.Error(). logger.Error().
Str("LockID", statRes.GetInfo().GetLock().GetLockId()). Str("LockID", statRes.GetInfo().GetLock().GetLockId()).
Msg("PutFile: wrong lock") Msg("PutFile: wrong lock")
// onlyoffice says it's required to send the current lockId, MS doesn't say anything // 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, // 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 { if lockID == "" && statRes.GetInfo().GetLock() == nil && statRes.GetInfo().GetSize() > 0 {
logger.Error().Msg("PutFile: file must be locked first") 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 // 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 // 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") Msg("UploadHelper: Put request to the upload endpoint failed with unexpected status")
return NewResponse(500), nil 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") logger.Debug().Msg("PutFile: success")
return NewResponse(200), nil return NewResponseWithVersion(200, mtime), nil
} }
@@ -6,14 +6,15 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"time"
"github.com/cs3org/reva/v2/pkg/utils"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1" appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/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" providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
revactx "github.com/cs3org/reva/v2/pkg/ctx" revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/rgrpc/status" "github.com/cs3org/reva/v2/pkg/rgrpc/status"
@@ -52,7 +53,6 @@ var _ = Describe("ContentConnector", func() {
}, },
Path: ".", Path: ".",
}, },
User: &userv1beta1.User{}, // Not used for now
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE, ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
} }
@@ -77,15 +77,28 @@ var _ = Describe("ContentConnector", func() {
}) })
Describe("GetFile", 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() { It("No valid context", func() {
sb := &strings.Builder{} sb := httptest.NewRecorder()
ctx := context.Background() ctx := context.Background()
err := cc.GetFile(ctx, sb) err := cc.GetFile(ctx, sb)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
It("Initiate download failed", func() { It("Initiate download failed", func() {
sb := &strings.Builder{} sb := httptest.NewRecorder()
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx) ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
targetErr := errors.New("Something went wrong") targetErr := errors.New("Something went wrong")
@@ -98,7 +111,7 @@ var _ = Describe("ContentConnector", func() {
}) })
It("Initiate download status not ok", func() { It("Initiate download status not ok", func() {
sb := &strings.Builder{} sb := httptest.NewRecorder()
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx) ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
gatewayClient.On("InitiateFileDownload", mock.Anything, mock.Anything).Times(1).Return(&gateway.InitiateFileDownloadResponse{ 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() { It("Missing download endpoint", func() {
sb := &strings.Builder{} sb := httptest.NewRecorder()
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx) ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
gatewayClient.On("InitiateFileDownload", mock.Anything, mock.Anything).Times(1).Return(&gateway.InitiateFileDownloadResponse{ 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() { It("Download request failed", func() {
sb := &strings.Builder{} sb := httptest.NewRecorder()
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx) ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
gatewayClient.On("InitiateFileDownload", mock.Anything, mock.Anything).Times(1).Return(&gateway.InitiateFileDownloadResponse{ 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() { It("Download request success", func() {
sb := &strings.Builder{} sb := httptest.NewRecorder()
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx) ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
gatewayClient.On("InitiateFileDownload", mock.Anything, mock.Anything).Times(1).Return(&gateway.InitiateFileDownloadResponse{ 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-Access-Token")).To(Equal(wopiCtx.AccessToken))
Expect(srvReqHeader.Get("X-Reva-Transfer")).To(Equal("MyDownloadToken")) Expect(srvReqHeader.Get("X-Reva-Transfer")).To(Equal("MyDownloadToken"))
Expect(err).To(Succeed()) Expect(err).To(Succeed())
Expect(sb.String()).To(Equal(randomContent)) Expect(sb.Body.String()).To(Equal(randomContent))
}) })
It("ViewOnlyMode Download request success", func() { It("ViewOnlyMode Download request success", func() {
sb := &strings.Builder{} sb := httptest.NewRecorder()
wopiCtx = middleware.WopiContext{ wopiCtx = middleware.WopiContext{
AccessToken: "abcdef123456", AccessToken: "abcdef123456",
@@ -184,7 +197,6 @@ var _ = Describe("ContentConnector", func() {
}, },
Path: ".", Path: ".",
}, },
User: &userv1beta1.User{}, // Not used for now
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY, 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-Access-Token")).To(Equal(wopiCtx.ViewOnlyToken))
Expect(srvReqHeader.Get("X-Reva-Transfer")).To(Equal("MyDownloadToken")) Expect(srvReqHeader.Get("X-Reva-Transfer")).To(Equal("MyDownloadToken"))
Expect(err).To(Succeed()) 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) }, nil)
response, err := cc.PutFile(ctx, reader, reader.Size(), "notARandomLockId") 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.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -264,9 +276,11 @@ var _ = Describe("ContentConnector", func() {
}, nil) }, nil)
response, err := cc.PutFile(ctx, reader, reader.Size(), "notARandomLockId") 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.Status).To(Equal(409))
Expect(response.Headers).To(HaveLen(2))
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("goodAndValidLock")) 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() { It("Upload without lockId but on a non empty file", func() {
@@ -282,7 +296,7 @@ var _ = Describe("ContentConnector", func() {
}, nil) }, nil)
response, err := cc.PutFile(ctx, reader, reader.Size(), "") response, err := cc.PutFile(ctx, reader, reader.Size(), "")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(409)) Expect(response.Status).To(Equal(409))
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("")) Expect(response.Headers[connector.HeaderWopiLock]).To(Equal(""))
}) })
@@ -332,7 +346,7 @@ var _ = Describe("ContentConnector", func() {
}, nil) }, nil)
response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock") 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.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -348,7 +362,8 @@ var _ = Describe("ContentConnector", func() {
LockId: "goodAndValidLock", LockId: "goodAndValidLock",
Type: providerv1beta1.LockType_LOCK_TYPE_WRITE, 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) }, nil)
@@ -357,9 +372,10 @@ var _ = Describe("ContentConnector", func() {
}, nil) }, nil)
response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock") 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.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() { It("Missing upload endpoint", func() {
@@ -382,7 +398,7 @@ var _ = Describe("ContentConnector", func() {
}, nil) }, nil)
response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock") 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.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -414,7 +430,7 @@ var _ = Describe("ContentConnector", func() {
response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock") response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock")
Expect(srvReqHeader.Get("X-Access-Token")).To(Equal(wopiCtx.AccessToken)) 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.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -434,6 +450,23 @@ var _ = Describe("ContentConnector", func() {
}, },
}, nil) }, 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{ gatewayClient.On("InitiateFileUpload", mock.Anything, mock.Anything).Times(1).Return(&gateway.InitiateFileUploadResponse{
Status: status.NewOK(ctx), Status: status.NewOK(ctx),
Protocols: []*gateway.FileUploadProtocol{ Protocols: []*gateway.FileUploadProtocol{
@@ -446,9 +479,10 @@ var _ = Describe("ContentConnector", func() {
response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock") response, err := cc.PutFile(ctx, reader, reader.Size(), "goodAndValidLock")
Expect(srvReqHeader.Get("X-Access-Token")).To(Equal(wopiCtx.AccessToken)) 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.Status).To(Equal(200))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(HaveLen(1))
Expect(response.Headers[connector.HeaderWopiVersion]).To(Equal("v16094592000"))
}) })
}) })
}) })
@@ -9,7 +9,6 @@ import (
"io" "io"
"net/url" "net/url"
"path" "path"
"strconv"
"strings" "strings"
"time" "time"
@@ -19,12 +18,15 @@ import (
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/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/cs3org/reva/v2/pkg/utils"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config" "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/connector/fileinfo"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/helpers" "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/middleware"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/wopisrc"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
@@ -52,7 +54,7 @@ type FileConnectorService interface {
// needs to be provided. // needs to be provided.
// The current lockID will be returned if a conflict happens // The current lockID will be returned if a conflict happens
RefreshLock(ctx context.Context, lockID string) (*ConnectorResponse, error) 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. // provided.
// The current lockID will be returned if a conflict happens // The current lockID will be returned if a conflict happens
UnLock(ctx context.Context, lockID string) (*ConnectorResponse, error) 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. // the method will return an empty lock id.
// //
// For the "unlock and relock" operation, the behavior will be the same. // 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) { func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (*ConnectorResponse, error) {
wopiContext, err := middleware.WopiContextFromCtx(ctx) wopiContext, err := middleware.WopiContextFromCtx(ctx)
if err != nil { if err != nil {
@@ -236,11 +240,26 @@ func (f *FileConnector) Lock(ctx context.Context, lockID, oldLockID string) (*Co
setOrRefreshStatus = resp.GetStatus() 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 // we're checking the status of either the "SetLock" or "RefreshLock" operations
switch setOrRefreshStatus.GetCode() { switch setOrRefreshStatus.GetCode() {
case rpcv1beta1.Code_CODE_OK: case rpcv1beta1.Code_CODE_OK:
logger.Debug().Msg("SetLock successful") 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: case rpcv1beta1.Code_CODE_FAILED_PRECONDITION, rpcv1beta1.Code_CODE_ABORTED:
// Code_CODE_FAILED_PRECONDITION -> Lock operation mismatched lock // 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(). logger.Warn().
Str("LockID", resp.GetLock().GetLockId()). Str("LockID", resp.GetLock().GetLockId()).
Msg("SetLock conflict") 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 // 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(). logger.Warn().
Str("LockID", resp.GetLock().GetLockId()). Str("LockID", resp.GetLock().GetLockId()).
Msg("SetLock lock refreshed instead") 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") 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. // return an empty lock id.
// The conflict happens if the provided lockID doesn't match the one actually // The conflict happens if the provided lockID doesn't match the one actually
// applied in the target file. // 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) { func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (*ConnectorResponse, error) {
wopiContext, err := middleware.WopiContextFromCtx(ctx) wopiContext, err := middleware.WopiContextFromCtx(ctx)
if err != nil { if err != nil {
@@ -347,10 +368,27 @@ func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (*Connec
return nil, err 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() { switch resp.GetStatus().GetCode() {
case rpcv1beta1.Code_CODE_OK: case rpcv1beta1.Code_CODE_OK:
logger.Debug().Msg("RefreshLock successful") 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: case rpcv1beta1.Code_CODE_NOT_FOUND:
logger.Error(). logger.Error().
@@ -390,7 +428,7 @@ func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (*Connec
Str("StatusCode", resp.GetStatus().GetCode().String()). Str("StatusCode", resp.GetStatus().GetCode().String()).
Str("StatusMsg", resp.GetStatus().GetMessage()). Str("StatusMsg", resp.GetStatus().GetMessage()).
Msg("RefreshLock failed, no lock on file") Msg("RefreshLock failed, no lock on file")
return NewResponseWithLock(409, ""), nil return NewResponseConflictWithVersion(statResp.GetInfo().GetMtime(), "No lock on file"), nil
} else { } else {
// lock is different than the one requested, otherwise we wouldn't reached this point // lock is different than the one requested, otherwise we wouldn't reached this point
logger.Error(). logger.Error().
@@ -398,7 +436,7 @@ func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (*Connec
Str("StatusCode", resp.GetStatus().GetCode().String()). Str("StatusCode", resp.GetStatus().GetCode().String()).
Str("StatusMsg", resp.GetStatus().GetMessage()). Str("StatusMsg", resp.GetStatus().GetMessage()).
Msg("RefreshLock failed, lock mismatch") Msg("RefreshLock failed, lock mismatch")
return NewResponseWithLock(409, resp.GetLock().GetLockId()), nil return NewResponseLockConflict(resp.GetLock().GetLockId(), "Lock mismatch"), nil
} }
default: default:
logger.Error(). logger.Error().
@@ -422,6 +460,8 @@ func (f *FileConnector) RefreshLock(ctx context.Context, lockID string) (*Connec
// return an empty lock id. // return an empty lock id.
// The conflict happens if the provided lockID doesn't match the one actually // The conflict happens if the provided lockID doesn't match the one actually
// applied in the target file. // 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) { func (f *FileConnector) UnLock(ctx context.Context, lockID string) (*ConnectorResponse, error) {
wopiContext, err := middleware.WopiContextFromCtx(ctx) wopiContext, err := middleware.WopiContextFromCtx(ctx)
if err != nil { if err != nil {
@@ -452,14 +492,29 @@ func (f *FileConnector) UnLock(ctx context.Context, lockID string) (*ConnectorRe
return nil, err 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() { switch resp.GetStatus().GetCode() {
case rpcv1beta1.Code_CODE_OK: case rpcv1beta1.Code_CODE_OK:
logger.Debug().Msg("Unlock successful") logger.Debug().Msg("Unlock successful")
return NewResponse(200), nil return NewResponseWithVersion(200, statResp.GetInfo().GetMtime()), nil
case rpcv1beta1.Code_CODE_ABORTED: case rpcv1beta1.Code_CODE_ABORTED:
// File isn't locked. Need to return 409 with empty lock // File isn't locked. Need to return 409 with empty lock
logger.Error().Err(err).Msg("Unlock failed, file isn't locked") 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: case rpcv1beta1.Code_CODE_LOCKED:
// We need to return 409 with the current lock // We need to return 409 with the current lock
req := &providerv1beta1.GetLockRequest{ req := &providerv1beta1.GetLockRequest{
@@ -496,7 +551,7 @@ func (f *FileConnector) UnLock(ctx context.Context, lockID string) (*ConnectorRe
Msg("Unlock failed, lock mismatch") Msg("Unlock failed, lock mismatch")
outLockId = resp.GetLock().GetLockId() outLockId = resp.GetLock().GetLockId()
} }
return NewResponseWithLock(409, outLockId), nil return NewResponseLockConflict(outLockId, "Lock mismatch"), nil
default: default:
logger.Error(). logger.Error().
Str("StatusCode", resp.GetStatus().GetCode().String()). Str("StatusCode", resp.GetStatus().GetCode().String()).
@@ -612,7 +667,7 @@ func (f *FileConnector) PutRelativeFileSuggested(ctx context.Context, ccs Conten
return nil, err return nil, err
} }
wopiSrcURL, err := f.generateWOPISrc(ctx, wopiContext, newLogger) wopiSrcURL, err := f.generateWOPISrc(wopiContext, newLogger)
if err != nil { if err != nil {
logger.Error().Err(err).Msg("PutRelativeFileSuggested: error generating the WOPISrc parameter") logger.Error().Err(err).Msg("PutRelativeFileSuggested: error generating the WOPISrc parameter")
return nil, err return nil, err
@@ -708,7 +763,7 @@ func (f *FileConnector) PutRelativeFileRelative(ctx context.Context, ccs Content
} }
// if conflict generate a different name and retry. // if conflict generate a different name and retry.
// this should happen only once // this should happen only once
wopiSrcURL, err2 := f.generateWOPISrc(ctx, wopiContext, newLogger) wopiSrcURL, err2 := f.generateWOPISrc(wopiContext, newLogger)
if err2 != nil { if err2 != nil {
newLogger.Error(). newLogger.Error().
Err(err2). Err(err2).
@@ -724,12 +779,13 @@ func (f *FileConnector) PutRelativeFileRelative(ctx context.Context, ccs Content
Str("LockID", lockID). Str("LockID", lockID).
Msg("PutRelativeFileRelative: error conflict") Msg("PutRelativeFileRelative: error conflict")
// need to build the response ourselves // need to build the response ourselves
return &ConnectorResponse{ return &ConnectorResponse{
Status: 409, Status: 409,
Headers: map[string]string{ Headers: map[string]string{
HeaderWopiValidRT: finalTarget, HeaderWopiValidRT: finalTarget,
HeaderWopiLock: lockID, HeaderWopiLock: lockID,
HeaderWopiLockFailureReason: "Lock Conflict",
}, },
Body: map[string]interface{}{ Body: map[string]interface{}{
"Name": target, "Name": target,
@@ -747,7 +803,7 @@ func (f *FileConnector) PutRelativeFileRelative(ctx context.Context, ccs Content
return nil, err return nil, err
} }
wopiSrcURL, err := f.generateWOPISrc(ctx, wopiContext, newLogger) wopiSrcURL, err := f.generateWOPISrc(wopiContext, newLogger)
if err != nil { if err != nil {
newLogger.Error().Err(err).Msg("PutRelativeFileRelative: error generating the WOPISrc parameter") newLogger.Error().Err(err).Msg("PutRelativeFileRelative: error generating the WOPISrc parameter")
return nil, err 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 { if deleteRes.GetStatus().GetCode() == rpcv1beta1.Code_CODE_TOO_EARLY {
// starting from 20ms, double the waiting time for each retry // starting from 20ms, double the waiting time for each retry
// capping at 5 secs // capping at 5 secs
waitingTime := (20 * time.Millisecond) << retries var waitingTime time.Duration
waitingTime = (20 * time.Millisecond) << retries
if waitingTime.Seconds() > 5 { if waitingTime.Seconds() > 5 {
waitingTime = 5 * time.Second waitingTime = 5 * time.Second
} }
@@ -849,7 +906,7 @@ func (f *FileConnector) DeleteFile(ctx context.Context, lockID string) (*Connect
logger.Error(). logger.Error().
Str("LockID", resp.GetLock().GetLockId()). Str("LockID", resp.GetLock().GetLockId()).
Msg("DeleteFile: file is locked") Msg("DeleteFile: file is locked")
return NewResponseWithLock(409, resp.GetLock().GetLockId()), nil return NewResponseLockConflict(resp.GetLock().GetLockId(), "File is locked"), nil
} else { } else {
// return the original error since the file isn't locked // return the original error since the file isn't locked
logger.Error().Msg("DeleteFile: delete failed on unlocked file") 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("StatusCode", moveRes.GetStatus().GetCode().String()).
Str("StatusMsg", moveRes.GetStatus().GetMessage()). Str("StatusMsg", moveRes.GetStatus().GetMessage()).
Msg("RenameFile: conflict") Msg("RenameFile: conflict")
return NewResponseWithLock(409, currentLockID), nil return NewResponseLockConflict(currentLockID, "Lock Conflict"), nil
} }
if moveRes.GetStatus().GetCode() == rpcv1beta1.Code_CODE_ALREADY_EXISTS { 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())) 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) // 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 // assign userId, userFriendlyName and isAnonymousUser
@@ -1029,30 +1085,44 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
isAnonymousUser := true isAnonymousUser := true
isPublicShare := false isPublicShare := false
if wopiContext.User != nil { user := ctxpkg.ContextMustGetUser(ctx)
if user.String() != "" {
// if we have a wopiContext.User // 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 { 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 isAnonymousUser = false
userFriendlyName = wopiContext.User.GetDisplayName() userFriendlyName = user.GetDisplayName()
userId = hexEncodedWopiUserId 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 // fileinfo map
infoMap := map[string]interface{}{ infoMap := map[string]interface{}{
fileinfo.KeyOwnerID: hexEncodedOwnerId, fileinfo.KeyOwnerID: hexEncodedOwnerId,
fileinfo.KeySize: int64(statRes.GetInfo().GetSize()), fileinfo.KeySize: int64(statRes.GetInfo().GetSize()),
fileinfo.KeyVersion: version, fileinfo.KeyVersion: helpers.GetVersion(statRes.GetInfo().GetMtime()),
fileinfo.KeyBaseFileName: path.Base(statRes.GetInfo().GetPath()), fileinfo.KeyBaseFileName: path.Base(statRes.GetInfo().GetPath()),
fileinfo.KeyBreadcrumbDocName: path.Base(statRes.GetInfo().GetPath()), fileinfo.KeyBreadcrumbDocName: path.Base(statRes.GetInfo().GetPath()),
// to get the folder we actually need to do a GetPath() request // 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: viewAppUrl.String(),
//fileinfo.KeyHostViewURL: wopiContext.ViewAppUrl, //fileinfo.KeyHostEditURL: editAppUrl.String(),
//fileinfo.KeyHostEditURL: wopiContext.EditAppUrl,
fileinfo.KeyEnableOwnerTermination: true, // only for collabora fileinfo.KeyEnableOwnerTermination: true, // only for collabora
fileinfo.KeySupportsExtendedLockLength: true, fileinfo.KeySupportsExtendedLockLength: true,
@@ -1066,7 +1136,8 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
fileinfo.KeyUserFriendlyName: userFriendlyName, fileinfo.KeyUserFriendlyName: userFriendlyName,
fileinfo.KeyUserID: userId, fileinfo.KeyUserID: userId,
fileinfo.KeyPostMessageOrigin: f.cfg.Commons.OcisURL, fileinfo.KeyPostMessageOrigin: f.cfg.Commons.OcisURL,
fileinfo.KeyLicenseCheckForEditIsEnabled: f.cfg.App.LicenseCheckEnable,
} }
switch wopiContext.ViewMode { switch wopiContext.ViewMode {
@@ -1082,7 +1153,7 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
infoMap[fileinfo.KeyDisableCopy] = true // only for collabora infoMap[fileinfo.KeyDisableCopy] = true // only for collabora
infoMap[fileinfo.KeyDisablePrint] = true infoMap[fileinfo.KeyDisablePrint] = true
if !isPublicShare { 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 // contains the resource id of the target file without the path
// (storage, opaque and space points directly to the file). The path component // (storage, opaque and space points directly to the file). The path component
// will be ignored // 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 // get the WOPI token for the new file
accessToken, _, err := middleware.GenerateWopiToken(wopiContext, f.cfg) accessToken, _, err := middleware.GenerateWopiToken(wopiContext, f.cfg)
if err != nil { if err != nil {
@@ -1174,16 +1245,14 @@ func (f *FileConnector) generateWOPISrc(ctx context.Context, wopiContext middlew
fileRef := helpers.HashResourceId(wopiContext.FileReference.GetResourceId()) fileRef := helpers.HashResourceId(wopiContext.FileReference.GetResourceId())
// generate the URL for the WOPI app to access the new created file // 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 { if err != nil {
logger.Error().Err(err).Msg("generateWOPISrc: failed to generate WOPISrc URL for the new file") logger.Error().Err(err).Msg("generateWOPISrc: failed to generate WOPISrc URL for the new file")
return nil, err return nil, err
} }
wopiSrcURL.Path = path.Join("wopi", "files", fileRef)
q := wopiSrcURL.Query() q := wopiSrcURL.Query()
q.Add("access_token", accessToken) q.Add("access_token", accessToken)
wopiSrcURL.RawQuery = q.Encode() wopiSrcURL.RawQuery = q.Encode()
return wopiSrcURL, nil return wopiSrcURL, nil
} }
@@ -12,6 +12,7 @@ import (
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/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" "github.com/cs3org/reva/v2/pkg/rgrpc/status"
cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks" cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
@@ -63,25 +64,6 @@ var _ = Describe("FileConnector", func() {
}, },
Path: ".", 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, ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
} }
}) })
@@ -116,7 +98,7 @@ var _ = Describe("FileConnector", func() {
}, nil) }, nil)
response, err := fc.GetLock(ctx) response, err := fc.GetLock(ctx)
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(404)) Expect(response.Status).To(Equal(404))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -134,7 +116,7 @@ var _ = Describe("FileConnector", func() {
}, nil) }, nil)
response, err := fc.GetLock(ctx) response, err := fc.GetLock(ctx)
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(200)) Expect(response.Status).To(Equal(200))
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999")) Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
}) })
@@ -153,7 +135,7 @@ var _ = Describe("FileConnector", func() {
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx) ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
response, err := fc.Lock(ctx, "", "") response, err := fc.Lock(ctx, "", "")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(400)) Expect(response.Status).To(Equal(400))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -179,10 +161,25 @@ var _ = Describe("FileConnector", func() {
Status: status.NewOK(ctx), Status: status.NewOK(ctx),
}, nil) }, 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", "") response, err := fc.Lock(ctx, "abcdef123", "")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(200)) 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() { It("Set lock mismatches error getting lock", func() {
@@ -197,6 +194,9 @@ var _ = Describe("FileConnector", func() {
Status: status.NewInternal(ctx, "lock mismatch"), Status: status.NewInternal(ctx, "lock mismatch"),
}, targetErr) }, targetErr)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.Lock(ctx, "abcdef123", "") response, err := fc.Lock(ctx, "abcdef123", "")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(err).To(Equal(targetErr)) Expect(err).To(Equal(targetErr))
@@ -218,10 +218,15 @@ var _ = Describe("FileConnector", func() {
}, },
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.Lock(ctx, "abcdef123", "") response, err := fc.Lock(ctx, "abcdef123", "")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(409)) Expect(response.Status).To(Equal(409))
Expect(response.Headers).To(HaveLen(2))
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999")) 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() { It("Set lock mismatches but get lock matches", func() {
@@ -239,10 +244,20 @@ var _ = Describe("FileConnector", func() {
}, },
}, nil) }, 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", "") response, err := fc.Lock(ctx, "abcdef123", "")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(200)) 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() { It("Set lock mismatches but get lock doesn't return lockId", func() {
@@ -256,8 +271,11 @@ var _ = Describe("FileConnector", func() {
Status: status.NewOK(ctx), Status: status.NewOK(ctx),
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.Lock(ctx, "abcdef123", "") response, err := fc.Lock(ctx, "abcdef123", "")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(500)) Expect(response.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -269,8 +287,11 @@ var _ = Describe("FileConnector", func() {
Status: status.NewNotFound(ctx, "file not found"), Status: status.NewNotFound(ctx, "file not found"),
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.Lock(ctx, "abcdef123", "") response, err := fc.Lock(ctx, "abcdef123", "")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(404)) Expect(response.Status).To(Equal(404))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -282,8 +303,11 @@ var _ = Describe("FileConnector", func() {
Status: status.NewInsufficientStorage(ctx, nil, "file too big"), Status: status.NewInsufficientStorage(ctx, nil, "file too big"),
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.Lock(ctx, "abcdef123", "") response, err := fc.Lock(ctx, "abcdef123", "")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(500)) Expect(response.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -301,7 +325,7 @@ var _ = Describe("FileConnector", func() {
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx) ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
response, err := fc.Lock(ctx, "", "oldLock") response, err := fc.Lock(ctx, "", "oldLock")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(400)) Expect(response.Status).To(Equal(400))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -327,10 +351,19 @@ var _ = Describe("FileConnector", func() {
Status: status.NewOK(ctx), Status: status.NewOK(ctx),
}, nil) }, 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") response, err := fc.Lock(ctx, "abcdef123", "oldLock")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(200)) 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() { It("Refresh lock mismatches error getting lock", func() {
@@ -345,6 +378,9 @@ var _ = Describe("FileConnector", func() {
Status: status.NewInternal(ctx, "lock mismatch"), Status: status.NewInternal(ctx, "lock mismatch"),
}, targetErr) }, targetErr)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.Lock(ctx, "abcdef123", "112233") response, err := fc.Lock(ctx, "abcdef123", "112233")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(err).To(Equal(targetErr)) Expect(err).To(Equal(targetErr))
@@ -366,8 +402,11 @@ var _ = Describe("FileConnector", func() {
}, },
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.Lock(ctx, "abcdef123", "112233") response, err := fc.Lock(ctx, "abcdef123", "112233")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(409)) Expect(response.Status).To(Equal(409))
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999")) Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
}) })
@@ -387,10 +426,20 @@ var _ = Describe("FileConnector", func() {
}, },
}, nil) }, 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") response, err := fc.Lock(ctx, "abcdef123", "112233")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(200)) 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() { It("Refresh lock mismatches but get lock doesn't return lockId", func() {
@@ -404,8 +453,11 @@ var _ = Describe("FileConnector", func() {
Status: status.NewOK(ctx), Status: status.NewOK(ctx),
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.Lock(ctx, "abcdef123", "112233") response, err := fc.Lock(ctx, "abcdef123", "112233")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(500)) Expect(response.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -417,8 +469,11 @@ var _ = Describe("FileConnector", func() {
Status: status.NewNotFound(ctx, "file not found"), Status: status.NewNotFound(ctx, "file not found"),
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.Lock(ctx, "abcdef123", "112233") response, err := fc.Lock(ctx, "abcdef123", "112233")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(404)) Expect(response.Status).To(Equal(404))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -430,8 +485,11 @@ var _ = Describe("FileConnector", func() {
Status: status.NewInsufficientStorage(ctx, nil, "file too big"), Status: status.NewInsufficientStorage(ctx, nil, "file too big"),
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.Lock(ctx, "abcdef123", "112233") response, err := fc.Lock(ctx, "abcdef123", "112233")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(500)) Expect(response.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -441,7 +499,8 @@ var _ = Describe("FileConnector", func() {
Describe("RefreshLock", func() { Describe("RefreshLock", func() {
It("No valid context", func() { It("No valid context", func() {
ctx := context.Background() ctx := context.Background()
response, err := fc.RefreshLock(ctx, "newLock")
response, err := fc.RefreshLock(ctx, "")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(response).To(BeNil()) Expect(response).To(BeNil())
}) })
@@ -450,7 +509,7 @@ var _ = Describe("FileConnector", func() {
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx) ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
response, err := fc.RefreshLock(ctx, "") response, err := fc.RefreshLock(ctx, "")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(400)) Expect(response.Status).To(Equal(400))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -476,10 +535,19 @@ var _ = Describe("FileConnector", func() {
Status: status.NewOK(ctx), Status: status.NewOK(ctx),
}, nil) }, 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") response, err := fc.RefreshLock(ctx, "abcdef123")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(200)) 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() { It("Refresh lock file not found", func() {
@@ -489,8 +557,11 @@ var _ = Describe("FileConnector", func() {
Status: status.NewNotFound(ctx, "file not found"), Status: status.NewNotFound(ctx, "file not found"),
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.RefreshLock(ctx, "abcdef123") response, err := fc.RefreshLock(ctx, "abcdef123")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(404)) Expect(response.Status).To(Equal(404))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -507,6 +578,9 @@ var _ = Describe("FileConnector", func() {
Status: status.NewConflict(ctx, nil, "lock mismatch"), Status: status.NewConflict(ctx, nil, "lock mismatch"),
}, targetErr) }, targetErr)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.RefreshLock(ctx, "abcdef123") response, err := fc.RefreshLock(ctx, "abcdef123")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(err).To(Equal(targetErr)) Expect(err).To(Equal(targetErr))
@@ -524,8 +598,11 @@ var _ = Describe("FileConnector", func() {
Status: status.NewInternal(ctx, "lock mismatch"), Status: status.NewInternal(ctx, "lock mismatch"),
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.RefreshLock(ctx, "abcdef123") response, err := fc.RefreshLock(ctx, "abcdef123")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(500)) Expect(response.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -541,8 +618,11 @@ var _ = Describe("FileConnector", func() {
Status: status.NewOK(ctx), Status: status.NewOK(ctx),
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.RefreshLock(ctx, "abcdef123") response, err := fc.RefreshLock(ctx, "abcdef123")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(409)) Expect(response.Status).To(Equal(409))
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("")) Expect(response.Headers[connector.HeaderWopiLock]).To(Equal(""))
}) })
@@ -562,8 +642,11 @@ var _ = Describe("FileConnector", func() {
}, },
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.RefreshLock(ctx, "abcdef123") response, err := fc.RefreshLock(ctx, "abcdef123")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(409)) Expect(response.Status).To(Equal(409))
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999")) Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
}) })
@@ -575,8 +658,11 @@ var _ = Describe("FileConnector", func() {
Status: status.NewInsufficientStorage(ctx, nil, "file too big"), Status: status.NewInsufficientStorage(ctx, nil, "file too big"),
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.RefreshLock(ctx, "abcdef123") response, err := fc.RefreshLock(ctx, "abcdef123")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(500)) Expect(response.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -585,7 +671,8 @@ var _ = Describe("FileConnector", func() {
Describe("Unlock", func() { Describe("Unlock", func() {
It("No valid context", func() { It("No valid context", func() {
ctx := context.Background() ctx := context.Background()
response, err := fc.UnLock(ctx, "newLock")
response, err := fc.UnLock(ctx, "")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(response).To(BeNil()) Expect(response).To(BeNil())
}) })
@@ -594,7 +681,7 @@ var _ = Describe("FileConnector", func() {
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx) ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
response, err := fc.UnLock(ctx, "") response, err := fc.UnLock(ctx, "")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(400)) Expect(response.Status).To(Equal(400))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -620,10 +707,19 @@ var _ = Describe("FileConnector", func() {
Status: status.NewOK(ctx), Status: status.NewOK(ctx),
}, nil) }, 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") response, err := fc.UnLock(ctx, "abcdef123")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(200)) 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() { It("Unlock file isn't locked", func() {
@@ -633,8 +729,16 @@ var _ = Describe("FileConnector", func() {
Status: status.NewConflict(ctx, nil, "lock mismatch"), Status: status.NewConflict(ctx, nil, "lock mismatch"),
}, nil) }, 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") response, err := fc.UnLock(ctx, "abcdef123")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(409)) Expect(response.Status).To(Equal(409))
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("")) Expect(response.Headers[connector.HeaderWopiLock]).To(Equal(""))
}) })
@@ -651,6 +755,9 @@ var _ = Describe("FileConnector", func() {
Status: status.NewInternal(ctx, "something failed"), Status: status.NewInternal(ctx, "something failed"),
}, targetErr) }, targetErr)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.UnLock(ctx, "abcdef123") response, err := fc.UnLock(ctx, "abcdef123")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(err).To(Equal(targetErr)) Expect(err).To(Equal(targetErr))
@@ -668,8 +775,11 @@ var _ = Describe("FileConnector", func() {
Status: status.NewInternal(ctx, "something failed"), Status: status.NewInternal(ctx, "something failed"),
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.UnLock(ctx, "abcdef123") response, err := fc.UnLock(ctx, "abcdef123")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(500)) Expect(response.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -685,8 +795,11 @@ var _ = Describe("FileConnector", func() {
Status: status.NewOK(ctx), Status: status.NewOK(ctx),
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.UnLock(ctx, "abcdef123") response, err := fc.UnLock(ctx, "abcdef123")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(409)) Expect(response.Status).To(Equal(409))
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("")) Expect(response.Headers[connector.HeaderWopiLock]).To(Equal(""))
}) })
@@ -706,8 +819,11 @@ var _ = Describe("FileConnector", func() {
}, },
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.UnLock(ctx, "abcdef123") response, err := fc.UnLock(ctx, "abcdef123")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(409)) Expect(response.Status).To(Equal(409))
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999")) Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
}) })
@@ -719,8 +835,11 @@ var _ = Describe("FileConnector", func() {
Status: status.NewInsufficientStorage(ctx, nil, "file too big"), Status: status.NewInsufficientStorage(ctx, nil, "file too big"),
}, nil) }, nil)
gatewayClient.EXPECT().Stat(mock.Anything, mock.Anything).
Return(&providerv1beta1.StatResponse{Status: status.NewOK(ctx)}, nil)
response, err := fc.UnLock(ctx, "abcdef123") response, err := fc.UnLock(ctx, "abcdef123")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(500)) Expect(response.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -759,7 +878,7 @@ var _ = Describe("FileConnector", func() {
stream := strings.NewReader("This is the content of a file") stream := strings.NewReader("This is the content of a file")
response, err := fc.PutRelativeFileSuggested(ctx, ccs, stream, int64(stream.Len()), "newFile.txt") 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.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
Expect(response.Body).To(BeNil()) Expect(response.Body).To(BeNil())
@@ -813,7 +932,7 @@ var _ = Describe("FileConnector", func() {
}, nil) }, nil)
response, err := fc.PutRelativeFileSuggested(ctx, ccs, stream, int64(stream.Len()), "newDocument.docx") 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.Status).To(Equal(200))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
rBody := response.Body.(map[string]interface{}) rBody := response.Body.(map[string]interface{})
@@ -869,7 +988,7 @@ var _ = Describe("FileConnector", func() {
}, nil) }, nil)
response, err := fc.PutRelativeFileSuggested(ctx, ccs, stream, int64(stream.Len()), ".pdf") 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.Status).To(Equal(200))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
rBody := response.Body.(map[string]interface{}) rBody := response.Body.(map[string]interface{})
@@ -938,7 +1057,7 @@ var _ = Describe("FileConnector", func() {
}, nil) }, nil)
response, err := fc.PutRelativeFileSuggested(ctx, ccs, stream, int64(stream.Len()), ".pdf") 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.Status).To(Equal(200))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
rBody := response.Body.(map[string]interface{}) 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) 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") 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.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
Expect(response.Body).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") stream := strings.NewReader("This is the content of a file")
response, err := fc.PutRelativeFileRelative(ctx, ccs, stream, int64(stream.Len()), "newFile.txt") 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.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
Expect(response.Body).To(BeNil()) Expect(response.Body).To(BeNil())
@@ -1065,7 +1184,7 @@ var _ = Describe("FileConnector", func() {
}, nil) }, nil)
response, err := fc.PutRelativeFileRelative(ctx, ccs, stream, int64(stream.Len()), "newDocument.docx") 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.Status).To(Equal(200))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
rBody := response.Body.(map[string]interface{}) rBody := response.Body.(map[string]interface{})
@@ -1124,7 +1243,7 @@ var _ = Describe("FileConnector", func() {
}, nil) }, nil)
response, err := fc.PutRelativeFileRelative(ctx, ccs, stream, int64(stream.Len()), "convFile.pdf") 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.Status).To(Equal(409))
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999")) Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
Expect(response.Headers[connector.HeaderWopiValidRT]).To(MatchRegexp(`[a-zA-Z0-9_-] convFile\.pdf`)) 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) 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") 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.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
Expect(response.Body).To(BeNil()) Expect(response.Body).To(BeNil())
@@ -1223,7 +1342,7 @@ var _ = Describe("FileConnector", func() {
}, targetErr) }, targetErr)
response, err := fc.DeleteFile(ctx, "newlock") response, err := fc.DeleteFile(ctx, "newlock")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(404)) Expect(response.Status).To(Equal(404))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -1240,7 +1359,7 @@ var _ = Describe("FileConnector", func() {
}, nil) }, nil)
response, err := fc.DeleteFile(ctx, "newlock") response, err := fc.DeleteFile(ctx, "newlock")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(500)) Expect(response.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -1261,7 +1380,7 @@ var _ = Describe("FileConnector", func() {
}, nil) }, nil)
response, err := fc.DeleteFile(ctx, "newlock") response, err := fc.DeleteFile(ctx, "newlock")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(409)) Expect(response.Status).To(Equal(409))
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999")) Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
}) })
@@ -1278,7 +1397,7 @@ var _ = Describe("FileConnector", func() {
}, nil) }, nil)
response, err := fc.DeleteFile(ctx, "newlock") response, err := fc.DeleteFile(ctx, "newlock")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(500)) Expect(response.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -1291,7 +1410,7 @@ var _ = Describe("FileConnector", func() {
}, nil) }, nil)
response, err := fc.DeleteFile(ctx, "newlock") response, err := fc.DeleteFile(ctx, "newlock")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(200)) Expect(response.Status).To(Equal(200))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
}) })
@@ -1327,7 +1446,7 @@ var _ = Describe("FileConnector", func() {
}, nil) }, nil)
response, err := fc.RenameFile(ctx, "lockid", "newFile.doc") response, err := fc.RenameFile(ctx, "lockid", "newFile.doc")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(500)) Expect(response.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
Expect(response.Body).To(BeNil()) Expect(response.Body).To(BeNil())
@@ -1375,7 +1494,7 @@ var _ = Describe("FileConnector", func() {
}, nil) }, nil)
response, err := fc.RenameFile(ctx, "lockid", "newFile.doc") response, err := fc.RenameFile(ctx, "lockid", "newFile.doc")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(500)) Expect(response.Status).To(Equal(500))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
Expect(response.Body).To(BeNil()) Expect(response.Body).To(BeNil())
@@ -1399,7 +1518,7 @@ var _ = Describe("FileConnector", func() {
}, nil) }, nil)
response, err := fc.RenameFile(ctx, "lockid", "newFile.doc") response, err := fc.RenameFile(ctx, "lockid", "newFile.doc")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(409)) Expect(response.Status).To(Equal(409))
Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999")) Expect(response.Headers[connector.HeaderWopiLock]).To(Equal("zzz999"))
Expect(response.Body).To(BeNil()) Expect(response.Body).To(BeNil())
@@ -1442,7 +1561,7 @@ var _ = Describe("FileConnector", func() {
}, nil).Once() }, nil).Once()
response, err := fc.RenameFile(ctx, "zzz999", "newFile.doc") response, err := fc.RenameFile(ctx, "zzz999", "newFile.doc")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(200)) Expect(response.Status).To(Equal(200))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
rBody := response.Body.(map[string]interface{}) rBody := response.Body.(map[string]interface{})
@@ -1474,7 +1593,7 @@ var _ = Describe("FileConnector", func() {
}, nil).Once() }, nil).Once()
response, err := fc.RenameFile(ctx, "zzz999", "newFile.doc") response, err := fc.RenameFile(ctx, "zzz999", "newFile.doc")
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(200)) Expect(response.Status).To(Equal(200))
Expect(response.Headers).To(BeNil()) Expect(response.Headers).To(BeNil())
rBody := response.Body.(map[string]interface{}) rBody := response.Body.(map[string]interface{})
@@ -1512,13 +1631,21 @@ var _ = Describe("FileConnector", func() {
}, nil) }, nil)
response, err := fc.CheckFileInfo(ctx) response, err := fc.CheckFileInfo(ctx)
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(500)) Expect(response.Status).To(Equal(500))
Expect(response.Body).To(BeNil()) Expect(response.Body).To(BeNil())
}) })
It("Stat success", func() { It("Stat success", func() {
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx) 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{ gatewayClient.On("Stat", mock.Anything, mock.Anything).Times(1).Return(&providerv1beta1.StatResponse{
Status: status.NewOK(ctx), Status: status.NewOK(ctx),
@@ -1533,17 +1660,25 @@ var _ = Describe("FileConnector", func() {
Seconds: uint64(16273849), Seconds: uint64(16273849),
}, },
Path: "/path/to/test.txt", Path: "/path/to/test.txt",
// Other properties aren't used for now. Id: &providerv1beta1.ResourceId{
StorageId: "storageid",
OpaqueId: "opaqueid",
SpaceId: "spaceid",
},
}, },
}, nil) }, nil)
expectedFileInfo := &fileinfo.Microsoft{ expectedFileInfo := &fileinfo.Microsoft{
OwnerID: "61616262636340637573746f6d496470", // hex of aabbcc@customIdp OwnerID: "61616262636340637573746f6d496470", // hex of aabbcc@customIdp
Size: int64(998877), Size: int64(998877),
Version: "16273849.0", Version: "v162738490",
BaseFileName: "test.txt", BaseFileName: "test.txt",
BreadcrumbDocName: "test.txt", BreadcrumbDocName: "test.txt",
UserCanNotWriteRelative: false, 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, SupportsExtendedLockLength: true,
SupportsGetLock: true, SupportsGetLock: true,
SupportsLocks: true, SupportsLocks: true,
@@ -1552,19 +1687,20 @@ var _ = Describe("FileConnector", func() {
SupportsRename: true, SupportsRename: true,
UserCanWrite: true, UserCanWrite: true,
UserCanRename: true, UserCanRename: true,
UserID: "6f7061717565496440696e6d656d6f7279", // hex of opaqueId@inmemory UserID: "61646d696e40637573746f6d496470", // hex of admin@customIdp
UserFriendlyName: "Pet Shaft", UserFriendlyName: "Pet Shaft",
} }
response, err := fc.CheckFileInfo(ctx) response, err := fc.CheckFileInfo(ctx)
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(200)) Expect(response.Status).To(Equal(200))
Expect(response.Body.(*fileinfo.Microsoft)).To(Equal(expectedFileInfo)) Expect(response.Body.(*fileinfo.Microsoft)).To(Equal(expectedFileInfo))
}) })
It("Stat success guests", func() { It("Stat success guests", func() {
// add user's opaque to include public-share-role // 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{ Map: map[string]*typesv1beta1.OpaqueEntry{
"public-share-role": &typesv1beta1.OpaqueEntry{ "public-share-role": &typesv1beta1.OpaqueEntry{
Decoder: "plain", Decoder: "plain",
@@ -1576,6 +1712,7 @@ var _ = Describe("FileConnector", func() {
wopiCtx.ViewMode = appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY wopiCtx.ViewMode = appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx) ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx)
ctx = ctxpkg.ContextSetUser(ctx, u)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Times(1).Return(&providerv1beta1.StatResponse{ gatewayClient.On("Stat", mock.Anything, mock.Anything).Times(1).Return(&providerv1beta1.StatResponse{
Status: status.NewOK(ctx), Status: status.NewOK(ctx),
@@ -1590,6 +1727,11 @@ var _ = Describe("FileConnector", func() {
Seconds: uint64(16273849), Seconds: uint64(16273849),
}, },
Path: "/path/to/test.txt", Path: "/path/to/test.txt",
Id: &providerv1beta1.ResourceId{
StorageId: "storageid",
OpaqueId: "opaqueid",
SpaceId: "spaceid",
},
// Other properties aren't used for now. // Other properties aren't used for now.
}, },
}, nil) }, nil)
@@ -1625,7 +1767,7 @@ var _ = Describe("FileConnector", func() {
response.Body.(*fileinfo.Collabora).UserID = "guest-zzz000" response.Body.(*fileinfo.Collabora).UserID = "guest-zzz000"
response.Body.(*fileinfo.Collabora).UserFriendlyName = "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.Status).To(Equal(200))
Expect(response.Body.(*fileinfo.Collabora)).To(Equal(expectedFileInfo)) Expect(response.Body.(*fileinfo.Collabora)).To(Equal(expectedFileInfo))
}) })
@@ -1635,6 +1777,16 @@ var _ = Describe("FileConnector", func() {
wopiCtx.ViewMode = appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY wopiCtx.ViewMode = appproviderv1beta1.ViewMode_VIEW_MODE_VIEW_ONLY
ctx := middleware.WopiContextToCtx(context.Background(), wopiCtx) 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{ gatewayClient.On("Stat", mock.Anything, mock.Anything).Times(1).Return(&providerv1beta1.StatResponse{
Status: status.NewOK(ctx), Status: status.NewOK(ctx),
@@ -1649,7 +1801,11 @@ var _ = Describe("FileConnector", func() {
Seconds: uint64(16273849), Seconds: uint64(16273849),
}, },
Path: "/path/to/test.txt", Path: "/path/to/test.txt",
// Other properties aren't used for now. Id: &providerv1beta1.ResourceId{
StorageId: "storageid",
OpaqueId: "opaqueid",
SpaceId: "spaceid",
},
}, },
}, nil) }, nil)
@@ -1664,7 +1820,7 @@ var _ = Describe("FileConnector", func() {
DisableExport: true, DisableExport: true,
DisableCopy: true, DisableCopy: true,
DisablePrint: true, DisablePrint: true,
UserID: hex.EncodeToString([]byte("opaqueId@inmemory")), UserID: hex.EncodeToString([]byte("aabbcc@example.com")),
UserFriendlyName: "Pet Shaft", UserFriendlyName: "Pet Shaft",
EnableOwnerTermination: true, EnableOwnerTermination: true,
WatermarkText: "Pet Shaft shaft@example.com", WatermarkText: "Pet Shaft shaft@example.com",
@@ -1677,7 +1833,7 @@ var _ = Describe("FileConnector", func() {
response, err := fc.CheckFileInfo(ctx) response, err := fc.CheckFileInfo(ctx)
Expect(err).To(Succeed()) Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(200)) Expect(response.Status).To(Equal(200))
Expect(response.Body.(*fileinfo.Collabora)).To(Equal(expectedFileInfo)) Expect(response.Body.(*fileinfo.Collabora)).To(Equal(expectedFileInfo))
}) })
@@ -60,7 +60,7 @@ type Microsoft struct {
// A Boolean value indicating whether the user is an education user or not. // A Boolean value indicating whether the user is an education user or not.
IsEduUser bool `json:"IsEduUser,omitempty"` IsEduUser bool `json:"IsEduUser,omitempty"`
// A Boolean value indicating whether the user is a business user or not. // 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. // A string that is the name of the user, suitable for displaying in UI.
UserFriendlyName string `json:"UserFriendlyName,omitempty"` 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. // A string value containing information about the user. This string can be passed from a WOPI client to the host by means of a PutUserInfo operation. If the host has a UserInfo string for the user, they must include it in this property. See the PutUserInfo documentation for more details.
@@ -5,7 +5,6 @@ import (
"errors" "errors"
"net/http" "net/http"
"strconv" "strconv"
"strings"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config" "github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
@@ -15,16 +14,18 @@ import (
) )
const ( const (
HeaderWopiLock string = "X-WOPI-Lock" HeaderWopiLock string = "X-WOPI-Lock"
HeaderWopiOldLock string = "X-WOPI-OldLock" HeaderWopiOldLock string = "X-WOPI-OldLock"
HeaderWopiST string = "X-WOPI-SuggestedTarget" HeaderWopiLockFailureReason string = "X-WOPI-LockFailureReason"
HeaderWopiRT string = "X-WOPI-RelativeTarget" HeaderWopiST string = "X-WOPI-SuggestedTarget"
HeaderWopiOverwriteRT string = "X-WOPI-OverwriteRelativeTarget" HeaderWopiRT string = "X-WOPI-RelativeTarget"
HeaderWopiSize string = "X-WOPI-Size" HeaderWopiOverwriteRT string = "X-WOPI-OverwriteRelativeTarget"
HeaderWopiValidRT string = "X-WOPI-ValidRelativeTarget" HeaderWopiSize string = "X-WOPI-Size"
HeaderWopiRequestedName string = "X-WOPI-RequestedName" HeaderWopiValidRT string = "X-WOPI-ValidRelativeTarget"
HeaderContentLength string = "Content-Length" HeaderWopiRequestedName string = "X-WOPI-RequestedName"
HeaderContentType string = "Content-Type" HeaderContentLength string = "Content-Length"
HeaderContentType string = "Content-Type"
HeaderWopiVersion string = "X-WOPI-ItemVersion"
) )
// HttpAdapter will adapt the responses from the connector to HTTP. // 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{} httpAdapter.locks = &locks.NoopLockParser{}
if strings.ToLower(cfg.App.Name) == "microsoftofficeonline" {
httpAdapter.locks = &locks.LegacyLockParser{}
}
return httpAdapter return httpAdapter
} }
@@ -7,6 +7,7 @@ import (
"net/http/httptest" "net/http/httptest"
"strings" "strings"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"github.com/owncloud/ocis/v2/services/collaboration/mocks" "github.com/owncloud/ocis/v2/services/collaboration/mocks"
@@ -133,12 +134,13 @@ var _ = Describe("HttpAdapter", func() {
w := httptest.NewRecorder() 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) httpAdapter.Lock(w, req)
resp := w.Result() resp := w.Result()
Expect(resp.StatusCode).To(Equal(409)) Expect(resp.StatusCode).To(Equal(409))
Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111")) Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111"))
Expect(resp.Header.Get(connector.HeaderWopiLockFailureReason)).To(Equal("Lock Conflict"))
}) })
It("Success", func() { It("Success", func() {
@@ -148,11 +150,18 @@ var _ = Describe("HttpAdapter", func() {
w := httptest.NewRecorder() 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) httpAdapter.Lock(w, req)
resp := w.Result() resp := w.Result()
Expect(resp.StatusCode).To(Equal(200)) 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() 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) httpAdapter.Lock(w, req)
resp := w.Result() resp := w.Result()
Expect(resp.StatusCode).To(Equal(409)) Expect(resp.StatusCode).To(Equal(409))
Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111")) Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111"))
Expect(resp.Header.Get(connector.HeaderWopiLockFailureReason)).To(Equal("Lock Conflict"))
}) })
It("Success", func() { It("Success", func() {
@@ -211,11 +221,18 @@ var _ = Describe("HttpAdapter", func() {
w := httptest.NewRecorder() 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) httpAdapter.Lock(w, req)
resp := w.Result() resp := w.Result()
Expect(resp.StatusCode).To(Equal(200)) 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() 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) httpAdapter.RefreshLock(w, req)
resp := w.Result() resp := w.Result()
Expect(resp.StatusCode).To(Equal(409)) Expect(resp.StatusCode).To(Equal(409))
Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111")) Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111"))
Expect(resp.Header.Get(connector.HeaderWopiLockFailureReason)).To(Equal("Lock Conflict"))
}) })
It("Success", func() { It("Success", func() {
@@ -271,11 +289,18 @@ var _ = Describe("HttpAdapter", func() {
w := httptest.NewRecorder() 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) httpAdapter.RefreshLock(w, req)
resp := w.Result() resp := w.Result()
Expect(resp.StatusCode).To(Equal(200)) 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() 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) httpAdapter.UnLock(w, req)
resp := w.Result() resp := w.Result()
Expect(resp.StatusCode).To(Equal(409)) Expect(resp.StatusCode).To(Equal(409))
Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111")) Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111"))
Expect(resp.Header.Get(connector.HeaderWopiLockFailureReason)).To(Equal("Lock Conflict"))
}) })
It("Success", func() { It("Success", func() {
@@ -330,11 +356,15 @@ var _ = Describe("HttpAdapter", func() {
w := httptest.NewRecorder() 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) httpAdapter.UnLock(w, req)
resp := w.Result() resp := w.Result()
Expect(resp.StatusCode).To(Equal(200)) 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() 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) httpAdapter.PutFile(w, req)
resp := w.Result() resp := w.Result()
Expect(resp.StatusCode).To(Equal(409)) Expect(resp.StatusCode).To(Equal(409))
Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111")) Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111"))
Expect(resp.Header.Get(connector.HeaderWopiLockFailureReason)).To(Equal("Lock Conflict"))
}) })
It("Success", func() { It("Success", func() {
@@ -473,11 +505,18 @@ var _ = Describe("HttpAdapter", func() {
w := httptest.NewRecorder() 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) httpAdapter.PutFile(w, req)
resp := w.Result() resp := w.Result()
Expect(resp.StatusCode).To(Equal(200)) Expect(resp.StatusCode).To(Equal(200))
Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("abc123"))
Expect(resp.Header.Get(connector.HeaderWopiVersion)).To(Equal("v1234567"))
}) })
}) })
}) })
@@ -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]
}
@@ -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)
}
+1 -2
View File
@@ -7,7 +7,6 @@ package locks
import ( import (
"encoding/json" "encoding/json"
"strings"
) )
// LockParser is the interface that wraps the ParseLock method // 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. // If the JSON string is not in the expected format, the original lockID will be returned.
func (*LegacyLockParser) ParseLock(id string) string { func (*LegacyLockParser) ParseLock(id string) string {
var decodedValues map[string]interface{} 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 { if err != nil || len(decodedValues) == 0 {
return id return id
} }
@@ -0,0 +1,13 @@
package middleware_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestMiddleware(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Middleware Suite")
}
@@ -3,6 +3,7 @@ package middleware
import ( import (
"net/http" "net/http"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
) )
@@ -27,7 +28,6 @@ func CollaborationTracingMiddleware(next http.Handler) http.Handler {
wopiMethod := r.Header.Get("X-WOPI-Override") wopiMethod := r.Header.Get("X-WOPI-Override")
wopiFile := wopiContext.FileReference wopiFile := wopiContext.FileReference
wopiUser := wopiContext.User.GetId()
attrs := []attribute.KeyValue{ attrs := []attribute.KeyValue{
attribute.String("ocis.wopi.sessionid", r.Header.Get("X-WOPI-SessionId")), 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.opaque", wopiFile.GetResourceId().GetOpaqueId()),
attribute.String("ocis.wopi.resource.id.space", wopiFile.GetResourceId().GetSpaceId()), attribute.String("ocis.wopi.resource.id.space", wopiFile.GetResourceId().GetSpaceId()),
attribute.String("ocis.wopi.resource.path", wopiFile.GetPath()), 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...) span.SetAttributes(attrs...)
@@ -5,12 +5,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"regexp"
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1" 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" providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" 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/golang-jwt/jwt/v5"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config" "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/helpers"
@@ -29,7 +28,6 @@ type WopiContext struct {
AccessToken string AccessToken string
ViewOnlyToken string ViewOnlyToken string
FileReference *providerv1beta1.Reference FileReference *providerv1beta1.Reference
User *userv1beta1.User
ViewMode appproviderv1beta1.ViewMode ViewMode appproviderv1beta1.ViewMode
} }
@@ -45,8 +43,6 @@ type WopiContext struct {
// * A contextual zerologger containing information about the request // * A contextual zerologger containing information about the request
// and the WopiContext // and the WopiContext
func WopiContextAuthMiddleware(cfg *config.Config, next http.Handler) http.Handler { 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
accessToken := r.URL.Query().Get("access_token") accessToken := r.URL.Query().Get("access_token")
if accessToken == "" { 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) http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return 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 claims.WopiContext.AccessToken = wopiContextAccessToken
ctx = context.WithValue(ctx, wopiContextKey, claims.WopiContext) ctx = context.WithValue(ctx, wopiContextKey, claims.WopiContext)
// authentication for the CS3 api // authentication for the CS3 api
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, claims.WopiContext.AccessToken) ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, claims.WopiContext.AccessToken)
ctx = ctxpkg.ContextSetUser(ctx, user)
// include additional info in the context's logger // 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 // 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("WopiStamp", r.Header.Get("X-WOPI-TimeStamp")).
Str("FileReference", claims.WopiContext.FileReference.String()). Str("FileReference", claims.WopiContext.FileReference.String()).
Str("ViewMode", claims.WopiContext.ViewMode.String()). Str("ViewMode", claims.WopiContext.ViewMode.String()).
Str("Requester", claims.WopiContext.User.GetId().String()). Str("Requester", user.GetId().String()).
Logger() Logger()
ctx = wopiLogger.WithContext(ctx) ctx = wopiLogger.WithContext(ctx)
hashedRef := helpers.HashResourceId(claims.WopiContext.FileReference.GetResourceId()) hashedRef := helpers.HashResourceId(claims.WopiContext.FileReference.GetResourceId())
matches := fileIDregexp.FindStringSubmatch(r.URL.Path) fileID := helpers.ParseWopiFileID(cfg, r.URL.Path)
if len(matches) < 2 || matches[1] != hashedRef { if fileID != hashedRef {
wopiLogger.Error().Msg("file reference in the URL doesn't match the one inside the access token") 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) http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return return
@@ -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))
})
})
@@ -121,6 +121,7 @@ func prepareRoutes(r *chi.Mux, options Options) {
// authentication and wopi context // authentication and wopi context
return colabmiddleware.WopiContextAuthMiddleware(options.Config, h) return colabmiddleware.WopiContextAuthMiddleware(options.Config, h)
}, },
colabmiddleware.CollaborationTracingMiddleware, colabmiddleware.CollaborationTracingMiddleware,
) )
@@ -15,6 +15,7 @@ import (
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 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/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/utils" "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/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config" "github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
@@ -109,7 +110,6 @@ func (s *Service) OpenInApp(
AccessToken: req.GetAccessToken(), // it will be encrypted AccessToken: req.GetAccessToken(), // it will be encrypted
ViewOnlyToken: utils.ReadPlainFromOpaque(req.GetOpaque(), "viewOnlyToken"), ViewOnlyToken: utils.ReadPlainFromOpaque(req.GetOpaque(), "viewOnlyToken"),
FileReference: &providerFileRef, FileReference: &providerFileRef,
User: user,
ViewMode: req.GetViewMode(), 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 // so that all sessions on one file end on the same office server
fileRef := helpers.HashResourceId(req.GetResourceInfo().GetId()) fileRef := helpers.HashResourceId(req.GetResourceInfo().GetId())
wopiSrcURL, err := url.Parse(s.config.Wopi.WopiSrc) wopiSrcURL, err := wopisrc.GenerateWopiSrc(fileRef, s.config)
if err != nil { if err != nil {
return "", err return "", err
} }
wopiSrcURL.Path = path.Join("wopi", "files", fileRef)
q := u.Query() q := u.Query()
q.Add("WOPISrc", wopiSrcURL.String()) q.Add("WOPISrc", wopiSrcURL.String())
@@ -216,6 +215,38 @@ func (s *Service) addQueryToURL(baseURL string, req *appproviderv1beta1.OpenInAp
lang := utils.ReadPlainFromOpaque(req.GetOpaque(), "lang") 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 != "" { if lang != "" {
switch strings.ToLower(s.config.App.Name) { switch strings.ToLower(s.config.App.Name) {
case "collabora": case "collabora":
@@ -187,15 +187,60 @@ var _ = Describe("Discovery", func() {
Entry("Microsoft chat no lang", "Microsoft", "", false, "https://test.server.prv/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopiserver.test.prv%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"), Entry("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("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("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("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"), 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"), 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("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("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("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("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"), 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"), 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)))
})
}) })
}) })
@@ -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
}
@@ -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")
}
@@ -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"))
})
})
})
})