From 1f4a9adaa1cfbdefaea520759073d4f885e24ec4 Mon Sep 17 00:00:00 2001 From: Giuseppe Lo Presti Date: Tue, 7 Mar 2023 16:12:00 +0100 Subject: [PATCH] Fixed public link paths + OCM users in WOPI driver (#3694) * Fixed public link paths in wopi driver following frontend updates * added changelog * Extended changelog * Decorate friendly name in case of OCM users * wopi: introduced a usertype parameter on the open call * Support OCM shares as well on wopi apps * Updated docs --- changelog/unreleased/wopi-apps.md | 7 ++ .../grpc/services/ocminvitemanager/_index.md | 18 +++++ .../grpc/services/ocmshareprovider/_index.md | 15 ++-- .../packages/app/provider/wopi/_index.md | 22 +++--- .../packages/ocm/share/repository/_index.md | 7 ++ .../ocm/share/repository/nextcloud/_index.md | 18 +++++ pkg/app/provider/wopi/wopi.go | 79 +++++++++++++++---- pkg/auth/scope/ocmshare.go | 21 +++++ pkg/auth/scope/publicshare.go | 2 +- pkg/utils/utils.go | 4 +- 10 files changed, 152 insertions(+), 41 deletions(-) create mode 100644 changelog/unreleased/wopi-apps.md create mode 100644 docs/content/en/docs/config/grpc/services/ocminvitemanager/_index.md create mode 100644 docs/content/en/docs/config/packages/ocm/share/repository/_index.md create mode 100644 docs/content/en/docs/config/packages/ocm/share/repository/nextcloud/_index.md diff --git a/changelog/unreleased/wopi-apps.md b/changelog/unreleased/wopi-apps.md new file mode 100644 index 0000000000..f7601367cb --- /dev/null +++ b/changelog/unreleased/wopi-apps.md @@ -0,0 +1,7 @@ +Bugfix: updated public links URLs and users' display names in WOPI apps + +Public links have changed in the frontend and are reflected in folderurl +query parameter. Additionally, OCM shares are supported for the folderurl +and OCM users are decorated with their ID provider. + +https://github.com/cs3org/reva/pull/3694 diff --git a/docs/content/en/docs/config/grpc/services/ocminvitemanager/_index.md b/docs/content/en/docs/config/grpc/services/ocminvitemanager/_index.md new file mode 100644 index 0000000000..cc72d58efe --- /dev/null +++ b/docs/content/en/docs/config/grpc/services/ocminvitemanager/_index.md @@ -0,0 +1,18 @@ +--- +title: "ocminvitemanager" +linkTitle: "ocminvitemanager" +weight: 10 +description: > + Configuration for the ocminvitemanager service +--- + +# _struct: config_ + +{{% dir name="provider_domain" type="string" default="The same domain registered in the provider authorizer" %}} + [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/ocminvitemanager/ocminvitemanager.go#L55) +{{< highlight toml >}} +[grpc.services.ocminvitemanager] +provider_domain = "The same domain registered in the provider authorizer" +{{< /highlight >}} +{{% /dir %}} + diff --git a/docs/content/en/docs/config/grpc/services/ocmshareprovider/_index.md b/docs/content/en/docs/config/grpc/services/ocmshareprovider/_index.md index 782f91c925..950d60597a 100644 --- a/docs/content/en/docs/config/grpc/services/ocmshareprovider/_index.md +++ b/docs/content/en/docs/config/grpc/services/ocmshareprovider/_index.md @@ -6,18 +6,13 @@ description: > Configuration for the OCM Share Provider service --- -{{% pageinfo %}} -TODO -{{% /pageinfo %}} - -{{% dir name="driver" type="string" default="" %}} -Driver to use. If you use a Nextcloud or ownCloud 10 backend, -you should use the "nextcloud" driver, and than provide -further configuration for it in -grpc.services.ocmshareprovider.drivers.nextcloud. +# _struct: config_ +{{% dir name="provider_domain" type="string" default="The same domain registered in the provider authorizer" %}} + [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/ocmshareprovider/ocmshareprovider.go#L63) {{< highlight toml >}} [grpc.services.ocmshareprovider] -driver = "nextcloud" +provider_domain = "The same domain registered in the provider authorizer" {{< /highlight >}} {{% /dir %}} + 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 dec3634b10..6f190be387 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#L64) +Inherited from the appprovider. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L78) {{< 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#L65) +The IOP secret used to connect to the wopiserver. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L79) {{< 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#L66) +The wopiserver's URL. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L80) {{< 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#L67) +The App user-friendly name. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L81) {{< highlight toml >}} [app.provider.wopi] app_name = "" @@ -41,7 +41,7 @@ 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#L68) +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#L82) {{< highlight toml >}} [app.provider.wopi] app_icon_uri = "" @@ -49,7 +49,7 @@ app_icon_uri = "" {{% /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#L69) +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#L83) {{< highlight toml >}} [app.provider.wopi] folder_base_url = "" @@ -57,7 +57,7 @@ folder_base_url = "" {{% /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#L70) +The App URL. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L84) {{< highlight toml >}} [app.provider.wopi] app_url = "" @@ -65,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#L71) +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#L85) {{< highlight toml >}} [app.provider.wopi] app_int_url = "" @@ -73,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#L72) +The API key used by the app, if applicable. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L86) {{< highlight toml >}} [app.provider.wopi] app_api_key = "" @@ -81,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#L73) +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#L87) {{< highlight toml >}} [app.provider.wopi] jwt_secret = "" @@ -89,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#L74) +Specifies if the app can be opened only on desktop. [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/app/provider/wopi/wopi.go#L88) {{< highlight toml >}} [app.provider.wopi] app_desktop_only = false diff --git a/docs/content/en/docs/config/packages/ocm/share/repository/_index.md b/docs/content/en/docs/config/packages/ocm/share/repository/_index.md new file mode 100644 index 0000000000..49f372a256 --- /dev/null +++ b/docs/content/en/docs/config/packages/ocm/share/repository/_index.md @@ -0,0 +1,7 @@ +--- +title: "repository" +linkTitle: "repository" +weight: 10 +description: > + Configuration for the repository service +--- \ No newline at end of file diff --git a/docs/content/en/docs/config/packages/ocm/share/repository/nextcloud/_index.md b/docs/content/en/docs/config/packages/ocm/share/repository/nextcloud/_index.md new file mode 100644 index 0000000000..e1688d6eae --- /dev/null +++ b/docs/content/en/docs/config/packages/ocm/share/repository/nextcloud/_index.md @@ -0,0 +1,18 @@ +--- +title: "nextcloud" +linkTitle: "nextcloud" +weight: 10 +description: > + Configuration for the nextcloud service +--- + +# _struct: ShareManagerConfig_ + +{{% dir name="endpoint" type="string" default="" %}} +The Nextcloud backend endpoint for user check [[Ref]](https://github.com/cs3org/reva/tree/master/pkg/ocm/share/repository/nextcloud/nextcloud.go#L60) +{{< highlight toml >}} +[ocm.share.repository.nextcloud] +endpoint = "" +{{< /highlight >}} +{{% /dir %}} + diff --git a/pkg/app/provider/wopi/wopi.go b/pkg/app/provider/wopi/wopi.go index e0c86c5f70..64e8b7bdea 100644 --- a/pkg/app/provider/wopi/wopi.go +++ b/pkg/app/provider/wopi/wopi.go @@ -56,6 +56,20 @@ import ( "github.com/pkg/errors" ) +const publicLinkURLPrefix = "/files/link/public/" + +const ocmLinkURLPrefix = "/files/spaces/sciencemesh/" + +type userType string + +const ( + invalid userType = "invalid" + regular userType = "regular" + federated userType = "federated" + ocm userType = "ocm" + anonymous userType = "anonymous" +) + func init() { registry.Register("wopi", New) } @@ -111,7 +125,7 @@ func New(m map[string]interface{}) (app.Provider, error) { } wopiClient := rhttp.GetHTTPClient( - rhttp.Timeout(time.Duration(5*int64(time.Second))), + rhttp.Timeout(time.Duration(10*int64(time.Second))), rhttp.Insecure(c.InsecureConnections), ) wopiClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { @@ -146,6 +160,7 @@ func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.Resourc q.Add("viewmode", viewMode.String()) q.Add("appname", p.conf.AppName) + var ut = invalid u, ok := ctxpkg.ContextGetUser(ctx) if !ok { // we must have been authenticated @@ -153,10 +168,10 @@ func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.Resourc } 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) + ut = federated } else { q.Add("userid", u.Id.OpaqueId+"@"+u.Id.Idp) } - q.Add("username", u.DisplayName) scopes, ok := ctxpkg.ContextGetScopes(ctx) if !ok { @@ -166,19 +181,35 @@ func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.Resourc // 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//` + // either append `/files/spaces/` or the proper URL prefix + `/` 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") + var pathErr error + _, pubrole := utils.HasPublicShareRole(u) + _, ocmrole := utils.HasOCMShareRole(u) + switch { + case pubrole: + // we are in a public link, username is not set so it will default to "Guest xyz" + ut = anonymous + rPath, pathErr = getPathForExternalLink(ctx, scopes, resource, publicLinkURLPrefix) + if pathErr != nil { + log.Warn().Err(pathErr).Msg("wopi: failed to extract relative path from public link scope") } - } else { + case ocmrole: + // OCM users have no username: use displayname@Idp + ut = ocm + q.Add("username", u.DisplayName+" @ "+u.Id.Idp) + // and resolve the folder + rPath, pathErr = getPathForExternalLink(ctx, scopes, resource, ocmLinkURLPrefix) + if pathErr != nil { + log.Warn().Err(pathErr).Msg("wopi: failed to extract relative path from ocm link scope") + } + default: // in all other cases use the resource's path + if ut == invalid { + ut = regular + } rPath = "/files/spaces/" + path.Dir(resource.Path) + q.Add("username", u.DisplayName) } if rPath != "" { fu, err := url.JoinPath(p.conf.FolderBaseURL, rPath) @@ -188,6 +219,7 @@ func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.Resourc q.Add("folderurl", fu) } } + q.Add("usertype", string(ut)) var viewAppURL string if viewAppURLs, ok := p.appURLs["view"]; ok { @@ -470,13 +502,26 @@ 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) { +func getPathForExternalLink(ctx context.Context, scopes map[string]*authpb.Scope, resource *provider.ResourceInfo, pathPrefix string) (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") + ocmShares, err := scope.GetOCMSharesFromScopes(scopes) + if err != nil { + return "", err + } + var resID *provider.ResourceId + var token string + switch { + case len(pubShares) == 1: + resID = pubShares[0].ResourceId + token = pubShares[0].Token + case len(ocmShares) == 1: + resID = ocmShares[0].ResourceId + token = ocmShares[0].Token + default: + return "", errors.New("Either one public xor OCM share is supported, lookups not implemented") } client, err := pool.GetGatewayServiceClient(pool.Endpoint(sharedconf.GetGatewaySVC(""))) @@ -485,7 +530,7 @@ func getPathForPublicLink(ctx context.Context, scopes map[string]*authpb.Scope, } statRes, err := client.Stat(ctx, &provider.StatRequest{ Ref: &provider.Reference{ - ResourceId: pubShares[0].ResourceId, + ResourceId: resID, }, }) if err != nil { @@ -494,7 +539,7 @@ func getPathForPublicLink(ctx context.Context, scopes map[string]*authpb.Scope, if statRes.Info.Path == resource.Path { // this is a direct link to the resource - return "/s/" + pubShares[0].Token, nil + return pathPrefix + token, nil } // otherwise we are in a subfolder of the public link relPath, err := filepath.Rel(statRes.Info.Path, resource.Path) @@ -504,5 +549,5 @@ func getPathForPublicLink(ctx context.Context, scopes map[string]*authpb.Scope, 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 + return path.Join(pathPrefix+token, path.Dir(relPath)), nil } diff --git a/pkg/auth/scope/ocmshare.go b/pkg/auth/scope/ocmshare.go index 19f7793b76..9a9fbcf37c 100644 --- a/pkg/auth/scope/ocmshare.go +++ b/pkg/auth/scope/ocmshare.go @@ -32,6 +32,7 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/utils" "github.com/rs/zerolog" ) @@ -134,3 +135,23 @@ func AddOCMShareScope(share *ocmv1beta1.Share, role authpb.Role, scopes map[stri } return scopes, nil } + +// GetOCMSharesFromScopes returns all OCM shares in the given scope. +func GetOCMSharesFromScopes(scopes map[string]*authpb.Scope) ([]*ocmv1beta1.Share, error) { + var shares []*ocmv1beta1.Share + for k, s := range scopes { + if strings.HasPrefix(k, "ocmshare:") { + res := s.Resource + if res.Decoder != "json" { + return nil, errtypes.InternalError("resource should be json encoded") + } + var share ocmv1beta1.Share + err := utils.UnmarshalJSONToProtoV1(res.Value, &share) + if err != nil { + return nil, err + } + shares = append(shares, &share) + } + } + return shares, nil +} diff --git a/pkg/auth/scope/publicshare.go b/pkg/auth/scope/publicshare.go index e70410bf21..692275440d 100644 --- a/pkg/auth/scope/publicshare.go +++ b/pkg/auth/scope/publicshare.go @@ -136,7 +136,7 @@ func AddPublicShareScope(share *link.PublicShare, role authpb.Role, scopes map[s return scopes, nil } -// GetPublicSharesFromScopes returns all the public shares in the given scope. +// GetPublicSharesFromScopes returns all 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 { diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index f2be17c4f5..17102c517d 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -380,7 +380,7 @@ func HasOCMShareRole(u *userpb.User) (string, bool) { return "", false } -// HasPermissions returns true if all permissions defined in the stuict toCheck +// HasPermissions returns true if all permissions defined in the struct toCheck // are set in the target. func HasPermissions(target, toCheck *provider.ResourcePermissions) bool { targetStruct := reflect.ValueOf(target).Elem() @@ -395,7 +395,7 @@ func HasPermissions(target, toCheck *provider.ResourcePermissions) bool { return true } -// UserIsLightweight returns true if the user is a lightweith +// UserIsLightweight returns true if the user is a lightweight // or federated account. func UserIsLightweight(u *userpb.User) bool { return u.Id.Type == userpb.UserType_USER_TYPE_FEDERATED ||