Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Whitelist apps #2095

Merged
merged 7 commits into from
Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog/unreleased/whitelist-apps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Enhancement: Whitelisting for apps

AppProvider supported mime types are now overridden in its configuration.
A friendly name, a description, an extension, an icon and a default app,
can be configured in the AppRegistry for each mime type.

https://github.com/cs3org/reva/pull/2095
15 changes: 15 additions & 0 deletions examples/storage-references/gateway.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ appauth = "localhost:15000"
[grpc.services.ocminvitemanager]
[grpc.services.ocmproviderauthorizer]

[grpc.services.appregistry]
[grpc.services.appregistry.drivers.static.mime_types]
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" = {"extension" = "docx", "name" = "Microsoft Word", "description" = "Microsoft Word document"}
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" = {"extension" = "xlsx", "name" = "Microsoft Excel", "description" = "Microsoft Excel document"}
"application/vnd.openxmlformats-officedocument.presentationml.presentation" = {"extension" = "pptx", "name" = "Microsoft PowerPoint", "description" = "Microsoft PowerPoint document"}
"application/vnd.oasis.opendocument.text" = {"extension" = "odt", "name" = "OpenDocument", "description" = "OpenDocument text document"}
"application/vnd.oasis.opendocument.spreadsheet" = {"extension" = "ods", "name" = "OpenSpreadsheet", "description" = "OpenDocument spreadsheet document"}
"application/vnd.oasis.opendocument.presentation" = {"extension" = "odp", "name" = "OpenPresentation", "description" = "OpenDocument presentation document"}
"text/plain" = {"extension" = "txt", "name" = "Text file", "description" = "Text file"}
"text/markdown" = {"extension" = "md", "name" = "Markdown file", "description" = "Markdown file"}
"application/vnd.jupyter" = {"extension" = "ipynb", "name" = "Jupyter Notebook", "description" = "Jupyter Notebook"}

[grpc.services.appprovider]
mime_types = ["text/plain"]

[http.services.datagateway]
[http.services.prometheus]
[http.services.ocmd]
Expand Down
7 changes: 7 additions & 0 deletions internal/grpc/services/appprovider/appprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type config struct {
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
AppProviderURL string `mapstructure:"app_provider_url"`
GatewaySvc string `mapstructure:"gatewaysvc"`
MimeTypes []string `mapstructure:"mime_types"` // define the mimetypes supported by the AppProvider
}

func (c *config) init() {
Expand Down Expand Up @@ -106,6 +107,12 @@ func (s *service) registerProvider() {
}
pInfo.Address = s.conf.AppProviderURL

// Add the supported mime types from the configuration
if len(s.conf.MimeTypes) != 0 {
pInfo.MimeTypes = s.conf.MimeTypes
log.Debug().Msg("app provider: overridden mimetype")
}

client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
if err != nil {
log.Error().Err(err).Msgf("error registering app provider: could not get gateway client")
Expand Down
61 changes: 59 additions & 2 deletions internal/grpc/services/appregistry/appregistry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func Test_ListAppProviders(t *testing.T) {
tests := []struct {
name string
providers map[string]interface{}
mimeTypes map[string]map[string]string
want *registrypb.ListAppProvidersResponse
}{
{
Expand All @@ -54,6 +55,20 @@ func Test_ListAppProviders(t *testing.T) {
"mimetypes": []string{"currently/ignored"},
},
},
mimeTypes: map[string]map[string]string{
"text/json": {
"extension": "json",
"name": "JSON File",
"icon": "https://example.org/icons&file=json.png",
"default_app": "some Address",
},
"currently/ignored": {
"extension": "unknown",
"name": "Ignored file",
"icon": "https://example.org/icons&file=unknown.png",
"default_app": "some Address",
},
},

// only Status and Providers will be asserted in the tests
want: &registrypb.ListAppProvidersResponse{
Expand All @@ -77,6 +92,7 @@ func Test_ListAppProviders(t *testing.T) {
{
name: "providers is nil",
providers: nil,
mimeTypes: nil,
want: &registrypb.ListAppProvidersResponse{
Status: &rpcv1beta1.Status{
Code: 1,
Expand All @@ -93,6 +109,7 @@ func Test_ListAppProviders(t *testing.T) {
{
name: "empty providers",
providers: map[string]interface{}{},
mimeTypes: map[string]map[string]string{},

// only Status and Providers will be asserted in the tests
want: &registrypb.ListAppProvidersResponse{
Expand All @@ -114,6 +131,7 @@ func Test_ListAppProviders(t *testing.T) {
providers: map[string]interface{}{
"some Address": nil,
},
mimeTypes: map[string]map[string]string{},

// only Status and Providers will be asserted in the tests
want: &registrypb.ListAppProvidersResponse{
Expand All @@ -129,7 +147,7 @@ func Test_ListAppProviders(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rr, err := static.New(map[string]interface{}{"Providers": tt.providers})
rr, err := static.New(map[string]interface{}{"providers": tt.providers, "mime_types": tt.mimeTypes})
if err != nil {
t.Errorf("could not create registry error = %v", err)
return
Expand Down Expand Up @@ -168,6 +186,45 @@ func Test_GetAppProviders(t *testing.T) {
},
}

mimeTypes := map[string]map[string]string{
"text/json": {
"extension": "json",
"name": "JSON File",
"icon": "https://example.org/icons&file=json.png",
"default_app": "some Address",
},
"text/xml": {
"extension": "xml",
"name": "XML File",
"icon": "https://example.org/icons&file=xml.png",
"default_app": "some Address",
},
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {
glpatcern marked this conversation as resolved.
Show resolved Hide resolved
"extension": "doc",
"name": "Word File",
"icon": "https://example.org/icons&file=doc.png",
"default_app": "some Address",
},
"application/vnd.oasis.opendocument.presentation": {
"extension": "odf",
"name": "OpenDocument File",
"icon": "https://example.org/icons&file=odf.png",
"default_app": "some Address",
},
"application/vnd.apple.installer+xml": {
"extension": "mpkg",
"name": "Mpkg File",
"icon": "https://example.org/icons&file=mpkg.png",
"default_app": "some Address",
},
"image/bmp": {
"extension": "bmp",
"name": "Image File",
"icon": "https://example.org/icons&file=bmp.png",
"default_app": "some Address",
},
}

tests := []struct {
name string
search *providerv1beta1.ResourceInfo
Expand Down Expand Up @@ -258,7 +315,7 @@ func Test_GetAppProviders(t *testing.T) {
},
}

rr, err := static.New(map[string]interface{}{"providers": providers})
rr, err := static.New(map[string]interface{}{"providers": providers, "mime_types": mimeTypes})
if err != nil {
t.Errorf("could not create registry error = %v", err)
return
Expand Down
63 changes: 52 additions & 11 deletions internal/grpc/services/gateway/appprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package gateway
import (
"context"
"crypto/tls"
"fmt"
"net/url"
"strings"

Expand Down Expand Up @@ -221,42 +222,82 @@ func (s *svc) findAppProvider(ctx context.Context, ri *storageprovider.ResourceI
return nil, err
}

// when app is empty it means the user assumes a default behaviour.
// From a web perspective, means the user click on the file itself.
// Normally the file will get downloaded but if a suitable application exists
// the behaviour will change from download to open the file with the app.
if app == "" {
// We need to get the default provider in case app is not set
// If the default isn't set as well, we'll return the first provider which matches the mimetype
// If app is empty means that we need to rely on "default" behaviour.
// We currently do not have user preferences implemented so the only default
// we can currently enforce is one configured by the system admins, the
// "system default".
// If a default is not set we raise an error rather that giving the user the first provider in the list
// as the list is built on init time and is not deterministic, giving the user different results on service
// reload.
res, err := c.GetDefaultAppProviderForMimeType(ctx, &registry.GetDefaultAppProviderForMimeTypeRequest{
MimeType: ri.MimeType,
})
if err == nil && res.Status.Code == rpc.Code_CODE_OK && res.Provider != nil {
if err != nil {
err = errors.Wrap(err, "gateway: error calling GetDefaultAppProviderForMimeType")
return nil, err

}

// we've found a provider
if res.Status.Code == rpc.Code_CODE_OK && res.Provider != nil {
return res.Provider, nil
}

// we did not find a default provider
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
err := errtypes.NotFound(fmt.Sprintf("gateway: default app rovider for mime type:%s not found", ri.MimeType))
return nil, err
}

// response code is something else, bubble up error
// if a default is not set we abort as returning the first application available is not
// deterministic for the end-user as it depends on initialization order of the app approviders with the registry.
// It also provides a good hint to the system admin to configure the defaults accordingly.
err = errtypes.InternalError(fmt.Sprintf("gateway: unexpected grpc response status:%s when calling GetDefaultAppProviderForMimeType", res.Status))
return nil, err
}

// app has been forced and is set, we try to get an app provider that can satisfy it
// Note that we ask for the list of all available providers for a given resource
// even though we're only interested into the one set by the "app" parameter.
// A better call will be to issue a (to be added) GetAppProviderByName(app) method
// to just what we ask for.
res, err := c.GetAppProviders(ctx, &registry.GetAppProvidersRequest{
ResourceInfo: ri,
})
if err != nil {
err = errors.Wrap(err, "gateway: error calling GetAppProviders")
return nil, err
}

// if the list of app providers is empty means we expect a CODE_NOT_FOUND in the response
if res.Status.Code != rpc.Code_CODE_OK {
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
return nil, errtypes.NotFound("gateway: app provider not found for resource: " + ri.String())
}
return nil, errtypes.InternalError("gateway: error finding app providers")
}

if app != "" {
for _, p := range res.Providers {
if p.Name == app {
return p, nil
}
// if we only have one app provider we verify that it matches the requested app name
if len(res.Providers) == 1 {
p := res.Providers[0]
if p.Name == app {
return p, nil
}
return nil, errtypes.NotFound("gateway: app provider not found: " + app)
// we return error if we return the wrong app provider
err = errtypes.InternalError(fmt.Sprintf("gateway: user asked for app %q and we gave %q", app, p.Name))
return nil, err
}

// As a fallback, return the first provider in the list
return res.Providers[0], nil
// we should never arrive to the point of having more than one
// provider for the given "app" parameters sent by the user
return nil, errtypes.InternalError(fmt.Sprintf("gateway: user requested app %q and we provided %d applications", app, len(res.Providers)))

}

func getGRPCConfig(opaque *typespb.Opaque) (bool, bool) {
Expand Down
31 changes: 26 additions & 5 deletions pkg/app/registry/static/static.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package static

import (
"context"
"fmt"
"strings"
"sync"

Expand All @@ -35,8 +36,17 @@ func init() {
registry.Register("static", New)
}

type mimeTypeConfig struct {
Extension string `mapstructure:"extension"`
Name string `mapstructure:"name"`
Description string `mapstructure:"description"`
Icon string `mapstructure:"icon"`
DefaultApp string `mapstructure:"default_app"`
}

type config struct {
Providers map[string]*registrypb.ProviderInfo `mapstructure:"providers"`
MimeTypes map[string]mimeTypeConfig `mapstructure:"mime_types"`
}

func (c *config) init() {
Expand Down Expand Up @@ -64,6 +74,7 @@ type mimeTypeIndex struct {
}

type reg struct {
config *config
providers map[string]*registrypb.ProviderInfo
mimetypes map[string]*mimeTypeIndex // map the mime type to the addresses of the corresponding providers
sync.RWMutex
Expand All @@ -78,6 +89,7 @@ func New(m map[string]interface{}) (app.Registry, error) {
c.init()

newReg := reg{
config: c,
providers: c.Providers,
mimetypes: make(map[string]*mimeTypeIndex),
}
Expand All @@ -89,7 +101,12 @@ func New(m map[string]interface{}) (app.Registry, error) {
if ok {
newReg.mimetypes[m].apps = append(newReg.mimetypes[m].apps, addr)
} else {
newReg.mimetypes[m] = &mimeTypeIndex{apps: []string{addr}}
// set a default app provider if provided
mime, in := c.MimeTypes[m]
if !in {
return nil, errtypes.NotFound(fmt.Sprintf("mimetype %s not found in the configuration", m))
}
newReg.mimetypes[m] = &mimeTypeIndex{apps: []string{addr}, defaultApp: mime.DefaultApp}
}
}
}
Expand Down Expand Up @@ -161,13 +178,17 @@ func (b *reg) ListSupportedMimeTypes(ctx context.Context) ([]*registrypb.MimeTyp
if _, ok := mtmap[m]; ok {
mtmap[m].AppProviders = append(mtmap[m].AppProviders, &t)
} else {
mime, ok := b.config.MimeTypes[m]
if !ok {
return nil, errtypes.NotFound(fmt.Sprintf("mimetype %s not found in the configuration", m))
}
mtmap[m] = &registrypb.MimeTypeInfo{
MimeType: m,
AppProviders: []*registrypb.ProviderInfo{&t},
Ext: "", // TODO fetch from config
Name: "",
Description: "",
Icon: "",
Ext: mime.Extension,
Name: mime.Name,
Description: mime.Description,
Icon: mime.Icon,
}
res = append(res, mtmap[m])
}
Expand Down