diff --git a/Dockerfile.revad-eos b/Dockerfile.revad-eos index f8ab28f311..59a1bc861e 100644 --- a/Dockerfile.revad-eos +++ b/Dockerfile.revad-eos @@ -16,7 +16,7 @@ # granted to it by virtue of its status as an Intergovernmental Organization # or submit itself to any jurisdiction. -FROM gitlab-registry.cern.ch/dss/eos/eos-all:4.8.66 +FROM gitlab-registry.cern.ch/dss/eos/eos-fusex:4.8.91 RUN yum -y update && yum clean all @@ -25,6 +25,13 @@ RUN yum install -y make git gcc libc-dev bash epel-release golang && \ yum clean all && \ rm -rf /var/cache/yum +ADD https://golang.org/dl/go1.19.linux-amd64.tar.gz \ + go1.19.linux-amd64.tar.gz + +RUN rm -rf /usr/local/go && \ + tar -C /usr/local -xzf go1.19.linux-amd64.tar.gz && \ + rm go1.19.linux-amd64.tar.gz + ENV PATH /go/bin:/usr/local/go/bin:$PATH ENV GOPATH /go diff --git a/changelog/unreleased/folderurl-for-apps.md b/changelog/unreleased/folderurl-for-apps.md new file mode 100644 index 0000000000..a30960338c --- /dev/null +++ b/changelog/unreleased/folderurl-for-apps.md @@ -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 diff --git a/docs/content/en/docs/config/packages/app/provider/wopi/_index.md b/docs/content/en/docs/config/packages/app/provider/wopi/_index.md index 631923d946..99bb136408 100644 --- a/docs/content/en/docs/config/packages/app/provider/wopi/_index.md +++ b/docs/content/en/docs/config/packages/app/provider/wopi/_index.md @@ -9,7 +9,7 @@ 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 @@ -17,7 +17,7 @@ mime_types = nil {{% /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 = "" @@ -25,7 +25,7 @@ iop_secret = "" {{% /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 = "" @@ -33,7 +33,7 @@ wopi_url = "" {{% /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 = "" @@ -41,15 +41,23 @@ app_name = "" {{% /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 = "" @@ -57,7 +65,7 @@ app_url = "" {{% /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 = "" @@ -65,7 +73,7 @@ app_int_url = "" {{% /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 = "" @@ -73,7 +81,7 @@ app_api_key = "" {{% /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 = "" @@ -81,7 +89,7 @@ jwt_secret = "" {{% /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 diff --git a/examples/storage-references/appprovider-codimd.toml b/examples/storage-references/appprovider-codimd.toml index da04af947a..c5697f490c 100644 --- a/examples/storage-references/appprovider-codimd.toml +++ b/examples/storage-references/appprovider-codimd.toml @@ -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" diff --git a/internal/grpc/interceptors/auth/auth.go b/internal/grpc/interceptors/auth/auth.go index 6741641269..f3fd9e8641 100644 --- a/internal/grpc/interceptors/auth/auth.go +++ b/internal/grpc/interceptors/auth/auth.go @@ -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" @@ -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) @@ -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") @@ -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 @@ -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) } } @@ -192,7 +196,7 @@ 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") @@ -200,6 +204,7 @@ func NewStream(m map[string]interface{}, unprotected []string) (grpc.StreamServe // 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) } @@ -219,24 +224,24 @@ 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 } @@ -244,17 +249,17 @@ func dismantleToken(ctx context.Context, tkn string, req interface{}, mgr token. // 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) { diff --git a/pkg/app/provider/wopi/wopi.go b/pkg/app/provider/wopi/wopi.go index 07a0f16f6c..a5937a31bf 100644 --- a/pkg/app/provider/wopi/wopi.go +++ b/pkg/app/provider/wopi/wopi.go @@ -28,6 +28,7 @@ import ( "net/url" "os" "path" + "path/filepath" "strconv" "strings" "time" @@ -35,17 +36,21 @@ import ( "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" @@ -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."` @@ -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://` and we + // either append `/files/spaces/` or `/s//` + 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) } } @@ -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) } @@ -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 +} diff --git a/pkg/auth/scope/publicshare.go b/pkg/auth/scope/publicshare.go index 7f63db3ec8..9f94029c3f 100644 --- a/pkg/auth/scope/publicshare.go +++ b/pkg/auth/scope/publicshare.go @@ -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 +} diff --git a/pkg/ctx/userctx.go b/pkg/ctx/userctx.go index 4addf0ebda..0743f644c6 100644 --- a/pkg/ctx/userctx.go +++ b/pkg/ctx/userctx.go @@ -21,6 +21,7 @@ package ctx import ( "context" + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" ) @@ -29,6 +30,7 @@ type key int const ( userKey key = iota tokenKey + scopeKey idKey ) @@ -62,3 +64,14 @@ func ContextGetUserID(ctx context.Context) (*userpb.UserId, bool) { func ContextSetUserID(ctx context.Context, id *userpb.UserId) context.Context { return context.WithValue(ctx, idKey, id) } + +// ContextSetScopes stores the scopes in the context. +func ContextSetScopes(ctx context.Context, scopes map[string]*authpb.Scope) context.Context { + return context.WithValue(ctx, scopeKey, scopes) +} + +// ContextGetScopes returns the scopes if set in the given context. +func ContextGetScopes(ctx context.Context) (map[string]*authpb.Scope, bool) { + s, ok := ctx.Value(scopeKey).(map[string]*authpb.Scope) + return s, ok +}