Skip to content

Commit

Permalink
Added support for user's scopes and folderurl for WOPI apps
Browse files Browse the repository at this point in the history
  • Loading branch information
glpatcern committed Nov 30, 2022
1 parent d549d84 commit bd1a15e
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 36 deletions.
12 changes: 12 additions & 0 deletions changelog/unreleased/folderurl-for-apps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Enhancement: implemented folderurl for WOPI apps

The folderurl is now populated for WOPI apps, such that
for owners and named shares it points to the containing
folder, and for public links it points to the appropriate
public link URL.

On the way, functions to manipulate the user's scope and
extract the eventual public link token(s) have been added,
coauthored with @gmgigi96.

https://github.com/cs3org/reva/pull/3494
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
1 change: 1 addition & 0 deletions examples/storage-references/appprovider-codimd.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ wopi_url = "http://0.0.0.0:8880/"
app_name = "CodiMD"
app_url = "https://your-codimd-server.org:3000"
app_int_url = "https://your-codimd-server.org:3000"
folder_base_url = "https://your-reva-frontend.org"
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
93 changes: 81 additions & 12 deletions pkg/app/provider/wopi/wopi.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,29 @@ 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"
typespb "github.com/cs3org/go-cs3apis/cs3/types/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 @@ -61,6 +66,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 @@ -141,18 +147,45 @@ 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)

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 `/files/spaces/<full_path>` or `/s/<pltoken>/<relative_path>`
var rPath string
if _, ok := utils.HasPublicShareRole(u); ok {
// we are in a public link
q.Del("username") // on public shares default to "Guest xyz"
var err error
rPath, err = getPathForPublicLink(ctx, scopes, resource)
if err != nil {
log.Warn().Err(err).Msg("wopi: failed to extract relative path from public link scope")
}

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"
}
} else {
// in all other cases use the resource's path
rPath = "/files/spaces/" + path.Dir(resource.Path)
}
if rPath != "" {
fu, err := url.JoinPath(p.conf.FolderBaseURL, rPath)
if err != nil {
log.Error().Err(err).Msg("wopi: failed to prepare folderurl parameter, folder_base_url may be malformed")
} else {
q.Add("folderurl", fu)
}
}

Expand Down Expand Up @@ -183,7 +216,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 @@ -437,3 +469,40 @@ 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
}

if statRes.Info.Path == resource.Path {
// this is a direct link to the resource
return "/s/" + pubShares[0].Token, nil
}
// otherwise we are in a subfolder of the public link
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("/files/public/show/"+pubShares[0].Token, path.Dir(relPath)), nil
}
Loading

0 comments on commit bd1a15e

Please sign in to comment.