feat: office 365 proxy support
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
package middleware_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestMiddleware(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Middleware Suite")
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package middleware
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
@@ -27,7 +28,6 @@ func CollaborationTracingMiddleware(next http.Handler) http.Handler {
|
||||
wopiMethod := r.Header.Get("X-WOPI-Override")
|
||||
|
||||
wopiFile := wopiContext.FileReference
|
||||
wopiUser := wopiContext.User.GetId()
|
||||
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String("ocis.wopi.sessionid", r.Header.Get("X-WOPI-SessionId")),
|
||||
@@ -36,9 +36,14 @@ func CollaborationTracingMiddleware(next http.Handler) http.Handler {
|
||||
attribute.String("ocis.wopi.resource.id.opaque", wopiFile.GetResourceId().GetOpaqueId()),
|
||||
attribute.String("ocis.wopi.resource.id.space", wopiFile.GetResourceId().GetSpaceId()),
|
||||
attribute.String("ocis.wopi.resource.path", wopiFile.GetPath()),
|
||||
attribute.String("ocis.wopi.user.idp", wopiUser.GetIdp()),
|
||||
attribute.String("ocis.wopi.user.opaque", wopiUser.GetOpaqueId()),
|
||||
attribute.String("ocis.wopi.user.type", wopiUser.GetType().String()),
|
||||
}
|
||||
|
||||
if wopiUser, ok := ctxpkg.ContextGetUser(r.Context()); ok {
|
||||
attrs = append(attrs, []attribute.KeyValue{
|
||||
attribute.String("ocis.wopi.user.idp", wopiUser.GetId().GetIdp()),
|
||||
attribute.String("ocis.wopi.user.opaque", wopiUser.GetId().GetOpaqueId()),
|
||||
attribute.String("ocis.wopi.user.type", wopiUser.GetId().GetType().String()),
|
||||
}...)
|
||||
}
|
||||
span.SetAttributes(attrs...)
|
||||
|
||||
|
||||
@@ -5,12 +5,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
rjwt "github.com/cs3org/reva/v2/pkg/token/manager/jwt"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/collaboration/pkg/helpers"
|
||||
@@ -29,7 +28,6 @@ type WopiContext struct {
|
||||
AccessToken string
|
||||
ViewOnlyToken string
|
||||
FileReference *providerv1beta1.Reference
|
||||
User *userv1beta1.User
|
||||
ViewMode appproviderv1beta1.ViewMode
|
||||
}
|
||||
|
||||
@@ -45,8 +43,6 @@ type WopiContext struct {
|
||||
// * A contextual zerologger containing information about the request
|
||||
// and the WopiContext
|
||||
func WopiContextAuthMiddleware(cfg *config.Config, next http.Handler) http.Handler {
|
||||
// compile a regexp here to extract the fileid from the URL
|
||||
fileIDregexp := regexp.MustCompile(`^/wopi/files/([0-9a-f]{64})(/.*)?$`)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
accessToken := r.URL.Query().Get("access_token")
|
||||
if accessToken == "" {
|
||||
@@ -76,11 +72,25 @@ func WopiContextAuthMiddleware(cfg *config.Config, next http.Handler) http.Handl
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
tokenManager, err := rjwt.New(map[string]interface{}{
|
||||
"secret": cfg.TokenManager.JWTSecret,
|
||||
"expires": int64(24 * 60 * 60),
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
user, _, err := tokenManager.DismantleToken(ctx, wopiContextAccessToken)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
claims.WopiContext.AccessToken = wopiContextAccessToken
|
||||
|
||||
ctx = context.WithValue(ctx, wopiContextKey, claims.WopiContext)
|
||||
// authentication for the CS3 api
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, claims.WopiContext.AccessToken)
|
||||
ctx = ctxpkg.ContextSetUser(ctx, user)
|
||||
|
||||
// include additional info in the context's logger
|
||||
// we might need to check https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/common-headers
|
||||
@@ -94,13 +104,13 @@ func WopiContextAuthMiddleware(cfg *config.Config, next http.Handler) http.Handl
|
||||
Str("WopiStamp", r.Header.Get("X-WOPI-TimeStamp")).
|
||||
Str("FileReference", claims.WopiContext.FileReference.String()).
|
||||
Str("ViewMode", claims.WopiContext.ViewMode.String()).
|
||||
Str("Requester", claims.WopiContext.User.GetId().String()).
|
||||
Str("Requester", user.GetId().String()).
|
||||
Logger()
|
||||
ctx = wopiLogger.WithContext(ctx)
|
||||
|
||||
hashedRef := helpers.HashResourceId(claims.WopiContext.FileReference.GetResourceId())
|
||||
matches := fileIDregexp.FindStringSubmatch(r.URL.Path)
|
||||
if len(matches) < 2 || matches[1] != hashedRef {
|
||||
fileID := helpers.ParseWopiFileID(cfg, r.URL.Path)
|
||||
if fileID != hashedRef {
|
||||
wopiLogger.Error().Msg("file reference in the URL doesn't match the one inside the access token")
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user