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 25, 2022
1 parent 79a7d13 commit 9cc1698
Show file tree
Hide file tree
Showing 7 changed files with 155 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
84 changes: 72 additions & 12 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 @@ -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 9cc1698

Please sign in to comment.