Skip to content

Commit

Permalink
Implemented folderurl for WOPI apps
Browse files Browse the repository at this point in the history
  • Loading branch information
glpatcern committed Nov 25, 2022
1 parent 79a7d13 commit 8f11f01
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 40 deletions.
28 changes: 18 additions & 10 deletions docs/content/en/docs/config/packages/app/provider/wopi/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,79 +9,87 @@ description: >
# _struct: config_

{{% dir name="mime_types" type="[]string" default=nil %}}
Inherited from the appprovider. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L58)
Inherited from the appprovider. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L63)
{{< highlight toml >}}
[app.provider.wopi]
mime_types = nil
{{< /highlight >}}
{{% /dir %}}

{{% dir name="iop_secret" type="string" default="" %}}
The IOP secret used to connect to the wopiserver. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L59)
The IOP secret used to connect to the wopiserver. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L64)
{{< highlight toml >}}
[app.provider.wopi]
iop_secret = ""
{{< /highlight >}}
{{% /dir %}}

{{% dir name="wopi_url" type="string" default="" %}}
The wopiserver's URL. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L60)
The wopiserver's URL. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L65)
{{< highlight toml >}}
[app.provider.wopi]
wopi_url = ""
{{< /highlight >}}
{{% /dir %}}

{{% dir name="app_name" type="string" default="" %}}
The App user-friendly name. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L61)
The App user-friendly name. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L66)
{{< highlight toml >}}
[app.provider.wopi]
app_name = ""
{{< /highlight >}}
{{% /dir %}}

{{% dir name="app_icon_uri" type="string" default="" %}}
A URI to a static asset which represents the app icon. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L62)
A URI to a static asset which represents the app icon. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L67)
{{< highlight toml >}}
[app.provider.wopi]
app_icon_uri = ""
{{< /highlight >}}
{{% /dir %}}

{{% dir name="folder_base_url" type="string" default="" %}}
The base URL to generate links to navigate back to the containing folder. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L68)
{{< highlight toml >}}
[app.provider.wopi]
folder_base_url = ""
{{< /highlight >}}
{{% /dir %}}

{{% dir name="app_url" type="string" default="" %}}
The App URL. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L63)
The App URL. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L69)
{{< highlight toml >}}
[app.provider.wopi]
app_url = ""
{{< /highlight >}}
{{% /dir %}}

{{% dir name="app_int_url" type="string" default="" %}}
The internal app URL in case of dockerized deployments. Defaults to AppURL [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L64)
The internal app URL in case of dockerized deployments. Defaults to AppURL [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L70)
{{< highlight toml >}}
[app.provider.wopi]
app_int_url = ""
{{< /highlight >}}
{{% /dir %}}

{{% dir name="app_api_key" type="string" default="" %}}
The API key used by the app, if applicable. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L65)
The API key used by the app, if applicable. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L71)
{{< highlight toml >}}
[app.provider.wopi]
app_api_key = ""
{{< /highlight >}}
{{% /dir %}}

{{% dir name="jwt_secret" type="string" default="" %}}
The JWT secret to be used to retrieve the token TTL. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L66)
The JWT secret to be used to retrieve the token TTL. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L72)
{{< highlight toml >}}
[app.provider.wopi]
jwt_secret = ""
{{< /highlight >}}
{{% /dir %}}

{{% dir name="app_desktop_only" type="bool" default=false %}}
Specifies if the app can be opened only on desktop. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L67)
Specifies if the app can be opened only on desktop. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L73)
{{< highlight toml >}}
[app.provider.wopi]
app_desktop_only = false
Expand Down
33 changes: 19 additions & 14 deletions internal/grpc/interceptors/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"time"

"github.com/bluele/gcache"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/cs3org/reva/pkg/appctx"
Expand Down Expand Up @@ -103,12 +104,13 @@ func NewUnary(m map[string]interface{}, unprotected []string) (grpc.UnaryServerI
// to decide the storage provider.
tkn, ok := ctxpkg.ContextGetToken(ctx)
if ok {
u, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr, true)
u, scopes, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr, true)
if err == nil {
if blockedUsers.IsBlocked(u.Username) {
return nil, status.Errorf(codes.PermissionDenied, "user %s blocked", u.Username)
}
ctx = ctxpkg.ContextSetUser(ctx, u)
ctx = ctxpkg.ContextSetScopes(ctx, scopes)
}
}
return handler(ctx, req)
Expand All @@ -121,8 +123,8 @@ func NewUnary(m map[string]interface{}, unprotected []string) (grpc.UnaryServerI
return nil, status.Errorf(codes.Unauthenticated, "auth: core access token not found")
}

// validate the token and ensure access to the resource is allowed
u, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr, false)
// scopes, validate the token and ensure access to the resource is allowed
u, scopes, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr, false)
if err != nil {
log.Warn().Err(err).Msg("access token is invalid")
return nil, status.Errorf(codes.PermissionDenied, "auth: core access token is invalid")
Expand All @@ -133,6 +135,7 @@ func NewUnary(m map[string]interface{}, unprotected []string) (grpc.UnaryServerI
}

ctx = ctxpkg.ContextSetUser(ctx, u)
ctx = ctxpkg.ContextSetScopes(ctx, scopes)
return handler(ctx, req)
}
return interceptor, nil
Expand Down Expand Up @@ -174,9 +177,10 @@ func NewStream(m map[string]interface{}, unprotected []string) (grpc.StreamServe
// to decide the storage provider.
tkn, ok := ctxpkg.ContextGetToken(ctx)
if ok {
u, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr, true)
u, scopes, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr, true)
if err == nil {
ctx = ctxpkg.ContextSetUser(ctx, u)
ctx = ctxpkg.ContextSetScopes(ctx, scopes)
ss = newWrappedServerStream(ctx, ss)
}
}
Expand All @@ -192,14 +196,15 @@ func NewStream(m map[string]interface{}, unprotected []string) (grpc.StreamServe
}

// validate the token and ensure access to the resource is allowed
u, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr, false)
u, scopes, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr, false)
if err != nil {
log.Warn().Err(err).Msg("access token is invalid")
return status.Errorf(codes.PermissionDenied, "auth: core access token is invalid")
}

// store user and core access token in context.
ctx = ctxpkg.ContextSetUser(ctx, u)
ctx = ctxpkg.ContextSetScopes(ctx, scopes)
wrapped := newWrappedServerStream(ctx, ss)
return handler(srv, wrapped)
}
Expand All @@ -219,42 +224,42 @@ func (ss *wrappedServerStream) Context() context.Context {
return ss.newCtx
}

func dismantleToken(ctx context.Context, tkn string, req interface{}, mgr token.Manager, gatewayAddr string, unprotected bool) (*userpb.User, error) {
func dismantleToken(ctx context.Context, tkn string, req interface{}, mgr token.Manager, gatewayAddr string, unprotected bool) (*userpb.User, map[string]*authpb.Scope, error) {
u, tokenScope, err := mgr.DismantleToken(ctx, tkn)
if err != nil {
return nil, err
return nil, nil, err
}

if unprotected {
return u, nil
return u, nil, nil
}

if sharedconf.SkipUserGroupsInToken() {
client, err := pool.GetGatewayServiceClient(pool.Endpoint(gatewayAddr))
if err != nil {
return nil, err
return nil, nil, err
}
groups, err := getUserGroups(ctx, u, client)
if err != nil {
return nil, err
return nil, nil, err
}
u.Groups = groups
}

// Check if access to the resource is in the scope of the token
ok, err := scope.VerifyScope(ctx, tokenScope, req)
if err != nil {
return nil, errtypes.InternalError("error verifying scope of access token")
return nil, nil, errtypes.InternalError("error verifying scope of access token")
}
if ok {
return u, nil
return u, tokenScope, nil
}

if err = expandAndVerifyScope(ctx, req, tokenScope, u, gatewayAddr, mgr); err != nil {
return nil, err
return nil, nil, err
}

return u, nil
return u, tokenScope, nil
}

func getUserGroups(ctx context.Context, u *userpb.User, client gatewayv1beta1.GatewayAPIClient) ([]string, error) {
Expand Down
86 changes: 73 additions & 13 deletions pkg/app/provider/wopi/wopi.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,28 @@ import (
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"

"github.com/beevik/etree"
appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/pkg/app"
"github.com/cs3org/reva/pkg/app/provider/registry"
"github.com/cs3org/reva/pkg/appctx"
"github.com/cs3org/reva/pkg/auth/scope"
ctxpkg "github.com/cs3org/reva/pkg/ctx"
"github.com/cs3org/reva/pkg/errtypes"
"github.com/cs3org/reva/pkg/mime"
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/pkg/rhttp"
"github.com/cs3org/reva/pkg/sharedconf"
"github.com/cs3org/reva/pkg/utils"
"github.com/golang-jwt/jwt"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
Expand All @@ -60,6 +65,7 @@ type config struct {
WopiURL string `mapstructure:"wopi_url" docs:";The wopiserver's URL."`
AppName string `mapstructure:"app_name" docs:";The App user-friendly name."`
AppIconURI string `mapstructure:"app_icon_uri" docs:";A URI to a static asset which represents the app icon."`
FolderBaseURL string `mapstructure:"folder_base_url" docs:";The base URL to generate links to navigate back to the containing folder."`
AppURL string `mapstructure:"app_url" docs:";The App URL."`
AppIntURL string `mapstructure:"app_int_url" docs:";The internal app URL in case of dockerized deployments. Defaults to AppURL"`
AppAPIKey string `mapstructure:"app_api_key" docs:";The API key used by the app, if applicable."`
Expand Down Expand Up @@ -140,19 +146,39 @@ func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.Resourc
q.Add("appname", p.conf.AppName)

u, ok := ctxpkg.ContextGetUser(ctx)
if ok { // else username defaults to "Guest xyz"
if u.Id.Type == userpb.UserType_USER_TYPE_LIGHTWEIGHT || u.Id.Type == userpb.UserType_USER_TYPE_FEDERATED {
q.Add("userid", resource.Owner.OpaqueId+"@"+resource.Owner.Idp)
} else {
q.Add("userid", u.Id.OpaqueId+"@"+u.Id.Idp)
}
if !ok {
// we must have been authenticated
return nil, errors.New("wopi: ContextGetUser failed")
}
if u.Id.Type == userpb.UserType_USER_TYPE_LIGHTWEIGHT || u.Id.Type == userpb.UserType_USER_TYPE_FEDERATED {
q.Add("userid", resource.Owner.OpaqueId+"@"+resource.Owner.Idp)
} else {
q.Add("userid", u.Id.OpaqueId+"@"+u.Id.Idp)
}
q.Add("username", u.DisplayName)

q.Add("username", u.DisplayName)
if u.Opaque != nil {
if _, ok := u.Opaque.Map["public-share-role"]; ok {
q.Del("username") // on public shares default to "Guest xyz"
}
scopes, ok := ctxpkg.ContextGetScopes(ctx)
if !ok {
// we must find at least one scope (as owner or sharee)
return nil, errors.New("wopi: ContextGetScopes failed")
}

// TODO (lopresti) consolidate with the templating implemented in the edge branch;
// here we assume the FolderBaseURL looks like `https://<hostname>` and we
// either append `/spaces/<full_path>` or `/s/<pltoken>/<relative_path>`

if _, ok := utils.HasPublicShareRole(u); ok {
// we are in a public link
q.Del("username") // on public shares default to "Guest xyz"
relPath, err := getPathForPublicLink(ctx, scopes, resource)
if err != nil {
log.Warn().Err(err).Msg("wopi: failed to extract relative path from public link scope")
} else {
q.Add("folderurl", path.Join(p.conf.FolderBaseURL+"/s", relPath))
}
} else {
// in all other cases this is the folder path
q.Add("folderurl", path.Join(p.conf.FolderBaseURL+"/files/spaces", resource.Path))
}

var viewAppURL string
Expand Down Expand Up @@ -182,7 +208,6 @@ func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.Resourc
if q.Get("appurl") == "" && q.Get("appviewurl") == "" {
return nil, errors.New("wopi: neither edit nor view app url found")
}

if p.conf.AppIntURL != "" {
q.Add("appinturl", p.conf.AppIntURL)
}
Expand Down Expand Up @@ -237,7 +262,7 @@ func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.Resourc
return nil, err
}
urlQuery := url.Query()
urlQuery.Set("ui", language) // OnlyOffice + Office365
urlQuery.Set("ui", language) // OnlyOffice + Office365getPathForPublicLink
urlQuery.Set("lang", language) // Collabora
urlQuery.Set("rs", language) // Office365, https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/online/discovery#dc_llcc
url.RawQuery = urlQuery.Encode()
Expand Down Expand Up @@ -431,3 +456,38 @@ func parseWopiDiscovery(body io.Reader) (map[string]map[string]string, error) {
}
return appURLs, nil
}

func getPathForPublicLink(ctx context.Context, scopes map[string]*authpb.Scope, resource *provider.ResourceInfo) (string, error) {
pubShares, err := scope.GetPublicSharesFromScopes(scopes)
if err != nil {
return "", err
}
if len(pubShares) > 1 {
return "", errors.New("More than one public share found in the scope, lookup not implemented")
}

client, err := pool.GetGatewayServiceClient(pool.Endpoint(sharedconf.GetGatewaySVC("")))
if err != nil {
return "", err
}

statRes, err := client.Stat(ctx, &provider.StatRequest{
Ref: &provider.Reference{
ResourceId: pubShares[0].ResourceId,
},
})
if err != nil {
return "", err
}

relPath, err := filepath.Rel(statRes.Info.Path, resource.Path)
if err != nil {
return "", err
}

if strings.HasPrefix(relPath, "../") {
return "", errors.New("scope path does not contain target resource")
}

return path.Join(pubShares[0].Token, relPath), nil
}
20 changes: 20 additions & 0 deletions pkg/auth/scope/publicshare.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,23 @@ func AddPublicShareScope(share *link.PublicShare, role authpb.Role, scopes map[s
}
return scopes, nil
}

// GetPublicSharesFromScopes returns all the public shares in the given scope.
func GetPublicSharesFromScopes(scopes map[string]*authpb.Scope) ([]*link.PublicShare, error) {
var shares []*link.PublicShare
for k, s := range scopes {
if strings.HasPrefix(k, "publicshare:") {
res := s.Resource
if res.Decoder != "json" {
return nil, errtypes.InternalError("resource should be json encoded")
}
var share link.PublicShare
err := utils.UnmarshalJSONToProtoV1(res.Value, &share)
if err != nil {
return nil, err
}
shares = append(shares, &share)
}
}
return shares, nil
}
Loading

0 comments on commit 8f11f01

Please sign in to comment.