Skip to content

Commit

Permalink
Replacement for TokenInfo endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
2403905 committed Apr 25, 2024
1 parent ef59ba2 commit eb411e7
Show file tree
Hide file tree
Showing 3 changed files with 307 additions and 13 deletions.
12 changes: 12 additions & 0 deletions changelog/unreleased/tokenInfo-endpoint-replacement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Enhancement: Replacement for TokenInfo Endpoint

The client should basically always send a PROPFIND to /dav/public-files/{sharetoken}

* authenticated clients accessing an internal link are redirected to the "real" resource (`/dav/spaces/{target-resource-id}
* authenticated clients accessing a pubic link (password protected or not) for a resource they already have access to are also redirected to the "real" resource. (and always need to supply the password)
* unauthenticated clients accessing an internal link get a 401 returned with WWW-Authenticate set to Bearer (so that the client knows that it need to get a token via the IDP login page.
* unauthenticated clients accessing a password protected link get a 401 returned with WWW-Authenticate set to Basic to indicate the requirement for needing the link's password.

https://github.com/cs3org/reva/pull/4653
https://github.com/owncloud/ocis/issues/8858
92 changes: 79 additions & 13 deletions internal/http/services/owncloud/ocdav/dav.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package ocdav

import (
"context"
"fmt"
"net/http"
"path"
"path/filepath"
Expand All @@ -28,20 +29,26 @@ import (
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/config"
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/conversions"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/rhttp/router"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
"google.golang.org/grpc/metadata"
)

const (
_trashbinPath = "trash-bin"

// WwwAuthenticate captures the Www-Authenticate header string.
WwwAuthenticate = "Www-Authenticate"
)

// DavHandler routes to the different sub handlers
Expand Down Expand Up @@ -269,13 +276,15 @@ func (h *DavHandler) Handler(s *svc) http.Handler {
case res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
fallthrough
case res.Status.Code == rpc.Code_CODE_UNAUTHENTICATED:
w.WriteHeader(http.StatusUnauthorized)
if hasValidBasicAuthHeader {
w.WriteHeader(http.StatusUnauthorized)
b, err := errors.Marshal(http.StatusUnauthorized, "Username or password was incorrect", "")
errors.HandleWebdavError(log, w, b, err)
return
}
b, err := errors.Marshal(http.StatusUnauthorized, "No 'Authorization: Basic' header found", "")
w.Header().Add(WwwAuthenticate, fmt.Sprintf("Basic realm=\"%s\", charset=\"UTF-8\"", r.Host))
w.WriteHeader(http.StatusUnauthorized)
b, err := errors.Marshal(http.StatusUnauthorized, "Authorization failed: Basic header found", "")
errors.HandleWebdavError(log, w, b, err)
return
case res.Status.Code == rpc.Code_CODE_NOT_FOUND:
Expand All @@ -286,14 +295,32 @@ func (h *DavHandler) Handler(s *svc) http.Handler {
return
}

ctx = ctxpkg.ContextSetToken(ctx, res.Token)
ctx = ctxpkg.ContextSetUser(ctx, res.User)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, res.Token)
// force the internal link
var isInternal bool
sRes, err := getInternalLinkStat(ctx, s.gatewaySelector, token)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if sRes.GetStatus().GetCode() == rpc.Code_CODE_UNAUTHENTICATED {
w.Header().Add(WwwAuthenticate, fmt.Sprintf("Bearer realm=\"%s\", charset=\"UTF-8\"", r.Host))
w.WriteHeader(http.StatusUnauthorized)
b, err := errors.Marshal(http.StatusUnauthorized, "Authorization failed: Bearer header found", "")
errors.HandleWebdavError(log, w, b, err)
return
}
if sRes.GetStatus().GetCode() == rpc.Code_CODE_OK {
isInternal = true
} else {
ctx = ctxpkg.ContextSetToken(ctx, res.Token)
ctx = ctxpkg.ContextSetUser(ctx, res.User)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, res.Token)

r = r.WithContext(ctx)
r = r.WithContext(ctx)
// the public share manager knew the token, but does the referenced target still exist?
sRes, err = getTokenStatInfo(ctx, s.gatewaySelector, token)
}

// the public share manager knew the token, but does the referenced target still exist?
sRes, err := getTokenStatInfo(ctx, s.gatewaySelector, token)
switch {
case err != nil:
log.Error().Err(err).Msg("error sending grpc stat request")
Expand All @@ -316,12 +343,20 @@ func (h *DavHandler) Handler(s *svc) http.Handler {
}
log.Debug().Interface("statInfo", sRes.Info).Msg("Stat info from public link token path")

ctx := ContextWithTokenStatInfo(ctx, sRes.Info)
r = r.WithContext(ctx)
if sRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER {
h.PublicFileHandler.Handler(s).ServeHTTP(w, r)
if isInternal {
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "spaces")
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
r = r.WithContext(ctx)
r.URL.Path = "/" + storagespace.FormatResourceID(*sRes.GetInfo().GetId())
h.SpacesHandler.Handler(s, nil).ServeHTTP(w, r)
} else {
h.PublicFolderHandler.Handler(s).ServeHTTP(w, r)
ctx := ContextWithTokenStatInfo(ctx, sRes.Info)
r = r.WithContext(ctx)
if sRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER {
h.PublicFileHandler.Handler(s).ServeHTTP(w, r)
} else {
h.PublicFolderHandler.Handler(s).ServeHTTP(w, r)
}
}

default:
Expand All @@ -332,6 +367,37 @@ func (h *DavHandler) Handler(s *svc) http.Handler {
})
}

func getInternalLinkStat(ctx context.Context, selector pool.Selectable[gatewayv1beta1.GatewayAPIClient], token string) (*provider.StatResponse, error) {
client, err := selector.Next()
if err != nil {
return nil, err
}
psRes, err := client.GetPublicShare(ctx, &link.GetPublicShareRequest{
Ref: &link.PublicShareReference{
Spec: &link.PublicShareReference_Token{
Token: token,
},
}})
if err != nil {
if strings.Contains(err.Error(), "core access token not found") {
return nil, nil
}
return nil, err
}
if psRes.Status.Code != rpc.Code_CODE_OK {
return nil, nil
}

role := conversions.RoleFromResourcePermissions(psRes.Share.Permissions.GetPermissions(), true)
if role.OCSPermissions() == 0 {
return client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{
ResourceId: psRes.GetShare().GetResourceId(),
}})

}
return nil, nil
}

func getTokenStatInfo(ctx context.Context, selector pool.Selectable[gatewayv1beta1.GatewayAPIClient], token string) (*provider.StatResponse, error) {
client, err := selector.Next()
if err != nil {
Expand Down
216 changes: 216 additions & 0 deletions internal/http/services/owncloud/ocdav/ocdav_blackbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,222 @@ var _ = Describe("ocdav", func() {
})
})

Describe("PROPFIND to public-file", func() {

BeforeEach(func() {
// set the dav endpoint to test
basePath = "/dav/public-files"

// setup the request
rr = httptest.NewRecorder()
req, err = http.NewRequest("PROPFIND", basePath+"/aySTemFFUcNudVD", strings.NewReader(""))
req.Host = ":9200"
Expect(err).ToNot(HaveOccurred())
req = req.WithContext(ctx)
})

When("the the link is public", func() {
It("returns a status Unauthorized", func() {

client.On("Authenticate", mock.Anything, mock.MatchedBy(func(req *cs3gateway.AuthenticateRequest) bool {
return req.Type == "publicshares" &&
strings.HasPrefix(req.ClientId, "aySTemFFUcNudVD") &&
strings.HasPrefix(req.ClientSecret, "signature||")
})).Return(&cs3gateway.AuthenticateResponse{
Status: status.NewUnauthenticated(ctx, nil, ""),
}, nil)

handler.Handler().ServeHTTP(rr, req)
Expect(rr).To(HaveHTTPStatus(http.StatusUnauthorized))
Expect(rr).To(HaveHTTPHeaderWithValue("WWW-Authenticate", "Basic realm=\":9200\", charset=\"UTF-8\""))
})
It("returns a Multistatus with the file properties", func() {
req.Header.Set("Authorization", "Basic cHVibGljOmAxcWF6WHN3Mg==")
ctx = ctxpkg.ContextSetUser(context.Background(), user)

user = &cs3user.User{Id: &cs3user.UserId{OpaqueId: "username"}, Username: "username"}
expectedPathPrefix := "/"

userspace = &cs3storageprovider.StorageSpace{
Opaque: &cs3types.Opaque{
Map: map[string]*cs3types.OpaqueEntry{
"path": {
Decoder: "plain",
Value: []byte("/public/aySTemFFUcNudVD"),
},
},
},

Id: &cs3storageprovider.StorageSpaceId{OpaqueId: storagespace.FormatResourceID(cs3storageprovider.ResourceId{StorageId: utils.PublicStorageProviderID,
SpaceId: utils.PublicStorageSpaceID,
OpaqueId: "aySTemFFUcNudVD"})},
Root: &cs3storageprovider.ResourceId{StorageId: utils.PublicStorageProviderID,
SpaceId: utils.PublicStorageSpaceID,
OpaqueId: "aySTemFFUcNudVD"},
Name: "",
SpaceType: "mountpoint",
}

client.On("Authenticate", mock.Anything, mock.MatchedBy(func(req *cs3gateway.AuthenticateRequest) bool {
return req.Type == "publicshares" &&
strings.HasPrefix(req.ClientId, "aySTemFFUcNudVD") &&
strings.HasPrefix(req.ClientSecret, "password|`1qazXsw2")
})).Return(&cs3gateway.AuthenticateResponse{
Status: status.NewOK(ctx),
Token: "valid-token",
User: user,
}, nil)

client.On("Stat", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.StatRequest) bool {
return utils.ResourceIDEqual(req.Ref.ResourceId, &cs3storageprovider.ResourceId{
StorageId: utils.PublicStorageProviderID,
SpaceId: utils.PublicStorageSpaceID,
OpaqueId: "aySTemFFUcNudVD",
})
})).Return(&cs3storageprovider.StatResponse{
Status: status.NewOK(ctx),
Info: &cs3storageprovider.ResourceInfo{Id: &cs3storageprovider.ResourceId{
StorageId: utils.PublicStorageProviderID,
SpaceId: utils.PublicStorageSpaceID,
OpaqueId: "aySTemFFUcNudVD",
},
Type: cs3storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER},
}, nil).Once()

client.On("ListStorageSpaces", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.ListStorageSpacesRequest) bool {
p := string(req.Opaque.Map["path"].Value)
return p == "/" || strings.HasPrefix(p, expectedPathPrefix)
})).Return(&cs3storageprovider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*cs3storageprovider.StorageSpace{userspace}, // FIXME we may need to return the /public storage provider id and mock it
}, nil)

client.On("Stat", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.StatRequest) bool {
return utils.ResourceIDEqual(req.Ref.ResourceId, &cs3storageprovider.ResourceId{
StorageId: utils.PublicStorageProviderID,
SpaceId: utils.PublicStorageSpaceID,
OpaqueId: "aySTemFFUcNudVD",
})
})).Return(&cs3storageprovider.StatResponse{
Status: status.NewOK(ctx),
Info: &cs3storageprovider.ResourceInfo{Id: &cs3storageprovider.ResourceId{
StorageId: utils.PublicStorageProviderID,
SpaceId: utils.PublicStorageSpaceID,
OpaqueId: "aySTemFFUcNudVD",
},
Type: cs3storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER},
}, nil).Once()

client.On("ListContainer", mock.Anything, mock.Anything).Return(&cs3storageprovider.ListContainerResponse{
Status: status.NewOK(context.Background()),
}, nil)

handler.Handler().ServeHTTP(rr, req)
Expect(rr).To(HaveHTTPStatus(http.StatusMultiStatus))
})
})

When("the the link is internal", func() {
It("returns a status Unauthorized", func() {

client.On("Authenticate", mock.Anything, mock.MatchedBy(func(req *cs3gateway.AuthenticateRequest) bool {
return req.Type == "publicshares" &&
strings.HasPrefix(req.ClientId, "aySTemFFUcNudVD") &&
strings.HasPrefix(req.ClientSecret, "signature||")
})).Return(&cs3gateway.AuthenticateResponse{
Status: status.NewUnauthenticated(ctx, nil, ""),
}, nil)

handler.Handler().ServeHTTP(rr, req)
Expect(rr).To(HaveHTTPStatus(http.StatusUnauthorized))
Expect(rr).To(HaveHTTPHeaderWithValue("WWW-Authenticate", "Basic realm=\":9200\", charset=\"UTF-8\""))
})
It("returns a Multistatus with the file properties", func() {
req.Header.Set("Authorization", "Basic cHVibGljOmAxcWF6WHN3Mg==")
ctx = ctxpkg.ContextSetUser(context.Background(), user)

user = &cs3user.User{Id: &cs3user.UserId{OpaqueId: "username"}, Username: "username"}
expectedPathPrefix := "/"

userspace = &cs3storageprovider.StorageSpace{
Opaque: &cs3types.Opaque{
Map: map[string]*cs3types.OpaqueEntry{
"path": {
Decoder: "plain",
Value: []byte("/public/aySTemFFUcNudVD"),
},
},
},

Id: &cs3storageprovider.StorageSpaceId{OpaqueId: storagespace.FormatResourceID(cs3storageprovider.ResourceId{StorageId: utils.PublicStorageProviderID,
SpaceId: utils.PublicStorageSpaceID,
OpaqueId: "aySTemFFUcNudVD"})},
Root: &cs3storageprovider.ResourceId{StorageId: utils.PublicStorageProviderID,
SpaceId: utils.PublicStorageSpaceID,
OpaqueId: "aySTemFFUcNudVD"},
Name: "",
SpaceType: "mountpoint",
}

client.On("Authenticate", mock.Anything, mock.MatchedBy(func(req *cs3gateway.AuthenticateRequest) bool {
return req.Type == "publicshares" &&
strings.HasPrefix(req.ClientId, "aySTemFFUcNudVD") &&
strings.HasPrefix(req.ClientSecret, "password|`1qazXsw2")
})).Return(&cs3gateway.AuthenticateResponse{
Status: status.NewOK(ctx),
Token: "valid-token",
User: user,
}, nil)

client.On("Stat", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.StatRequest) bool {
return utils.ResourceIDEqual(req.Ref.ResourceId, &cs3storageprovider.ResourceId{
StorageId: utils.PublicStorageProviderID,
SpaceId: utils.PublicStorageSpaceID,
OpaqueId: "aySTemFFUcNudVD",
})
})).Return(&cs3storageprovider.StatResponse{
Status: status.NewOK(ctx),
Info: &cs3storageprovider.ResourceInfo{Id: &cs3storageprovider.ResourceId{
StorageId: utils.PublicStorageProviderID,
SpaceId: utils.PublicStorageSpaceID,
OpaqueId: "aySTemFFUcNudVD",
},
Type: cs3storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER},
}, nil).Once()

client.On("ListStorageSpaces", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.ListStorageSpacesRequest) bool {
p := string(req.Opaque.Map["path"].Value)
return p == "/" || strings.HasPrefix(p, expectedPathPrefix)
})).Return(&cs3storageprovider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*cs3storageprovider.StorageSpace{userspace}, // FIXME we may need to return the /public storage provider id and mock it
}, nil)

client.On("Stat", mock.Anything, mock.MatchedBy(func(req *cs3storageprovider.StatRequest) bool {
return utils.ResourceIDEqual(req.Ref.ResourceId, &cs3storageprovider.ResourceId{
StorageId: utils.PublicStorageProviderID,
SpaceId: utils.PublicStorageSpaceID,
OpaqueId: "aySTemFFUcNudVD",
})
})).Return(&cs3storageprovider.StatResponse{
Status: status.NewOK(ctx),
Info: &cs3storageprovider.ResourceInfo{Id: &cs3storageprovider.ResourceId{
StorageId: utils.PublicStorageProviderID,
SpaceId: utils.PublicStorageSpaceID,
OpaqueId: "aySTemFFUcNudVD",
},
Type: cs3storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER},
}, nil).Once()

client.On("ListContainer", mock.Anything, mock.Anything).Return(&cs3storageprovider.ListContainerResponse{
Status: status.NewOK(context.Background()),
}, nil)

handler.Handler().ServeHTTP(rr, req)
Expect(rr).To(HaveHTTPStatus(http.StatusMultiStatus))
})
})
})
Describe("MKCOL", func() {

BeforeEach(func() {
Expand Down

0 comments on commit eb411e7

Please sign in to comment.