From ffd292da5052e86fc6e1e61fc7427d47729143e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Tue, 10 Jan 2023 22:43:59 +0000 Subject: [PATCH 01/23] initial webfinger stub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- ocis-pkg/config/config.go | 2 + ocis-pkg/config/defaultconfig.go | 2 + ocis/pkg/command/webfinger.go | 31 +++++ ocis/pkg/runtime/service/service.go | 2 + services/webfinger/.dockerignore | 2 + services/webfinger/Makefile | 39 ++++++ services/webfinger/README.md | 13 ++ services/webfinger/cmd/webfinger/main.go | 14 ++ services/webfinger/pkg/command/health.go | 54 ++++++++ services/webfinger/pkg/command/root.go | 59 +++++++++ services/webfinger/pkg/command/server.go | 112 ++++++++++++++++ services/webfinger/pkg/command/version.go | 50 +++++++ services/webfinger/pkg/config/config.go | 22 ++++ services/webfinger/pkg/config/debug.go | 9 ++ .../pkg/config/defaults/defaultconfig.go | 73 +++++++++++ services/webfinger/pkg/config/http.go | 20 +++ services/webfinger/pkg/config/log.go | 9 ++ services/webfinger/pkg/config/parser/parse.go | 37 ++++++ services/webfinger/pkg/config/service.go | 6 + services/webfinger/pkg/config/tracing.go | 9 ++ services/webfinger/pkg/logging/logging.go | 17 +++ services/webfinger/pkg/metrics/metrics.go | 81 ++++++++++++ services/webfinger/pkg/metrics/options.go | 31 +++++ services/webfinger/pkg/server/debug/option.go | 50 +++++++ services/webfinger/pkg/server/debug/server.go | 63 +++++++++ services/webfinger/pkg/server/http/option.go | 84 ++++++++++++ services/webfinger/pkg/server/http/server.go | 98 ++++++++++++++ .../webfinger/pkg/service/v0/instrument.go | 38 ++++++ services/webfinger/pkg/service/v0/logging.go | 31 +++++ services/webfinger/pkg/service/v0/option.go | 40 ++++++ services/webfinger/pkg/service/v0/service.go | 122 ++++++++++++++++++ services/webfinger/pkg/service/v0/tracing.go | 32 +++++ services/webfinger/pkg/tracing/tracing.go | 23 ++++ services/webfinger/pkg/webfinger/webfinger.go | 55 ++++++++ services/webfinger/reflex.conf | 2 + 35 files changed, 1332 insertions(+) create mode 100644 ocis/pkg/command/webfinger.go create mode 100644 services/webfinger/.dockerignore create mode 100644 services/webfinger/Makefile create mode 100644 services/webfinger/README.md create mode 100644 services/webfinger/cmd/webfinger/main.go create mode 100644 services/webfinger/pkg/command/health.go create mode 100644 services/webfinger/pkg/command/root.go create mode 100644 services/webfinger/pkg/command/server.go create mode 100644 services/webfinger/pkg/command/version.go create mode 100644 services/webfinger/pkg/config/config.go create mode 100644 services/webfinger/pkg/config/debug.go create mode 100644 services/webfinger/pkg/config/defaults/defaultconfig.go create mode 100644 services/webfinger/pkg/config/http.go create mode 100644 services/webfinger/pkg/config/log.go create mode 100644 services/webfinger/pkg/config/parser/parse.go create mode 100644 services/webfinger/pkg/config/service.go create mode 100644 services/webfinger/pkg/config/tracing.go create mode 100644 services/webfinger/pkg/logging/logging.go create mode 100644 services/webfinger/pkg/metrics/metrics.go create mode 100644 services/webfinger/pkg/metrics/options.go create mode 100644 services/webfinger/pkg/server/debug/option.go create mode 100644 services/webfinger/pkg/server/debug/server.go create mode 100644 services/webfinger/pkg/server/http/option.go create mode 100644 services/webfinger/pkg/server/http/server.go create mode 100644 services/webfinger/pkg/service/v0/instrument.go create mode 100644 services/webfinger/pkg/service/v0/logging.go create mode 100644 services/webfinger/pkg/service/v0/option.go create mode 100644 services/webfinger/pkg/service/v0/service.go create mode 100644 services/webfinger/pkg/service/v0/tracing.go create mode 100644 services/webfinger/pkg/tracing/tracing.go create mode 100644 services/webfinger/pkg/webfinger/webfinger.go create mode 100644 services/webfinger/reflex.conf diff --git a/ocis-pkg/config/config.go b/ocis-pkg/config/config.go index da0b57e625a..7133b27bc72 100644 --- a/ocis-pkg/config/config.go +++ b/ocis-pkg/config/config.go @@ -33,6 +33,7 @@ import ( users "github.com/owncloud/ocis/v2/services/users/pkg/config" web "github.com/owncloud/ocis/v2/services/web/pkg/config" webdav "github.com/owncloud/ocis/v2/services/webdav/pkg/config" + webfinger "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" ) const ( @@ -106,5 +107,6 @@ type Config struct { Users *users.Config `yaml:"users"` Web *web.Config `yaml:"web"` WebDAV *webdav.Config `yaml:"webdav"` + Webfinger *webfinger.Config `yaml:"webfinger"` Search *search.Config `yaml:"search"` } diff --git a/ocis-pkg/config/defaultconfig.go b/ocis-pkg/config/defaultconfig.go index b9f959223a1..fd527e95109 100644 --- a/ocis-pkg/config/defaultconfig.go +++ b/ocis-pkg/config/defaultconfig.go @@ -31,6 +31,7 @@ import ( users "github.com/owncloud/ocis/v2/services/users/pkg/config/defaults" web "github.com/owncloud/ocis/v2/services/web/pkg/config/defaults" webdav "github.com/owncloud/ocis/v2/services/webdav/pkg/config/defaults" + webfinger "github.com/owncloud/ocis/v2/services/webfinger/pkg/config/defaults" ) func DefaultConfig() *Config { @@ -71,5 +72,6 @@ func DefaultConfig() *Config { Users: users.DefaultConfig(), Web: web.DefaultConfig(), WebDAV: webdav.DefaultConfig(), + Webfinger: webfinger.DefaultConfig(), } } diff --git a/ocis/pkg/command/webfinger.go b/ocis/pkg/command/webfinger.go new file mode 100644 index 00000000000..e01fde8648a --- /dev/null +++ b/ocis/pkg/command/webfinger.go @@ -0,0 +1,31 @@ +package command + +import ( + "github.com/owncloud/ocis/v2/ocis-pkg/config" + "github.com/owncloud/ocis/v2/ocis-pkg/config/configlog" + "github.com/owncloud/ocis/v2/ocis-pkg/config/parser" + "github.com/owncloud/ocis/v2/ocis/pkg/command/helper" + "github.com/owncloud/ocis/v2/ocis/pkg/register" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/command" + "github.com/urfave/cli/v2" +) + +// WebfingerCommand is the entrypoint for the webfinger command. +func WebfingerCommand(cfg *config.Config) *cli.Command { + + return &cli.Command{ + Name: cfg.Webfinger.Service.Name, + Usage: helper.SubcommandDescription(cfg.Webfinger.Service.Name), + Category: "services", + Before: func(c *cli.Context) error { + configlog.Error(parser.ParseConfig(cfg, true)) + cfg.Webfinger.Commons = cfg.Commons + return nil + }, + Subcommands: command.GetCommands(cfg.Webfinger), + } +} + +func init() { + register.AddCommand(WebfingerCommand) +} diff --git a/ocis/pkg/runtime/service/service.go b/ocis/pkg/runtime/service/service.go index f05e5fc2002..84a25679f34 100644 --- a/ocis/pkg/runtime/service/service.go +++ b/ocis/pkg/runtime/service/service.go @@ -48,6 +48,7 @@ import ( users "github.com/owncloud/ocis/v2/services/users/pkg/command" web "github.com/owncloud/ocis/v2/services/web/pkg/command" webdav "github.com/owncloud/ocis/v2/services/webdav/pkg/command" + webfinger "github.com/owncloud/ocis/v2/services/webfinger/pkg/command" "github.com/thejerf/suture/v4" ) @@ -112,6 +113,7 @@ func NewService(options ...Option) (*Service, error) { s.ServicesRegistry[opts.Config.Thumbnails.Service.Name] = thumbnails.NewSutureService s.ServicesRegistry[opts.Config.Web.Service.Name] = web.NewSutureService s.ServicesRegistry[opts.Config.WebDAV.Service.Name] = webdav.NewSutureService + s.ServicesRegistry[opts.Config.Webfinger.Service.Name] = webfinger.NewSutureService s.ServicesRegistry[opts.Config.Frontend.Service.Name] = frontend.NewSutureService s.ServicesRegistry[opts.Config.OCDav.Service.Name] = ocdav.NewSutureService s.ServicesRegistry[opts.Config.Gateway.Service.Name] = gateway.NewSutureService diff --git a/services/webfinger/.dockerignore b/services/webfinger/.dockerignore new file mode 100644 index 00000000000..4ec85b5e4f7 --- /dev/null +++ b/services/webfinger/.dockerignore @@ -0,0 +1,2 @@ +* +!bin/ diff --git a/services/webfinger/Makefile b/services/webfinger/Makefile new file mode 100644 index 00000000000..fb4a6a37a8a --- /dev/null +++ b/services/webfinger/Makefile @@ -0,0 +1,39 @@ +SHELL := bash +NAME := webfinger + +include ../../.make/recursion.mk + +############ tooling ############ +ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI +include ../../.bingo/Variables.mk +endif + +############ go tooling ############ +include ../../.make/go.mk + +############ release ############ +include ../../.make/release.mk + +############ docs generate ############ +include ../../.make/docs.mk + +.PHONY: docs-generate +docs-generate: config-docs-generate + +############ generate ############ +include ../../.make/generate.mk + +.PHONY: ci-go-generate +ci-go-generate: $(MOCKERY) # CI runs ci-node-generate automatically before this target + $(MOCKERY) --srcpkg github.com/go-ldap/ldap/v3 --case underscore --filename ldapclient.go --name Client + + +.PHONY: ci-node-generate +ci-node-generate: + +############ licenses ############ +.PHONY: ci-node-check-licenses +ci-node-check-licenses: + +.PHONY: ci-node-save-licenses +ci-node-save-licenses: diff --git a/services/webfinger/README.md b/services/webfinger/README.md new file mode 100644 index 00000000000..0742d1c0ba1 --- /dev/null +++ b/services/webfinger/README.md @@ -0,0 +1,13 @@ +# Webfinger service + +The webfinger service provides an RFC7033 WebFinger lookup of ownCloud instances relevant for a given user account. + +It is based on https://github.com/owncloud/lookup-webfinger-sciebo but also returns a `displayname` in addition to the `href` property. + +# Links +Initial issue: https://github.com/owncloud/ocis/issues/5281 + +# TODO +- [ ] actually query a backend, for now ldap in context of a multi tenant deployment ... or use the graph api to list /education/schools for a user? ldap would be more direct. +- [ ] don't use the broken metrics/instrumentation/logging interface from graph / webdav services, see https://github.com/owncloud/ocis/issues/5209 + diff --git a/services/webfinger/cmd/webfinger/main.go b/services/webfinger/cmd/webfinger/main.go new file mode 100644 index 00000000000..f370d1c9e9e --- /dev/null +++ b/services/webfinger/cmd/webfinger/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "os" + + "github.com/owncloud/ocis/v2/services/webfinger/pkg/command" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config/defaults" +) + +func main() { + if err := command.Execute(defaults.DefaultConfig()); err != nil { + os.Exit(1) + } +} diff --git a/services/webfinger/pkg/command/health.go b/services/webfinger/pkg/command/health.go new file mode 100644 index 00000000000..f8f657b274f --- /dev/null +++ b/services/webfinger/pkg/command/health.go @@ -0,0 +1,54 @@ +package command + +import ( + "fmt" + "net/http" + + "github.com/owncloud/ocis/v2/ocis-pkg/config/configlog" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config/parser" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/logging" + "github.com/urfave/cli/v2" +) + +// Health is the entrypoint for the health command. +func Health(cfg *config.Config) *cli.Command { + return &cli.Command{ + Name: "health", + Usage: "check health status", + Category: "info", + Before: func(c *cli.Context) error { + return configlog.ReturnError(parser.ParseConfig(cfg)) + }, + Action: func(c *cli.Context) error { + logger := logging.Configure(cfg.Service.Name, cfg.Log) + + resp, err := http.Get( + fmt.Sprintf( + "http://%s/healthz", + cfg.Debug.Addr, + ), + ) + + if err != nil { + logger.Fatal(). + Err(err). + Msg("Failed to request health check") + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + logger.Fatal(). + Int("code", resp.StatusCode). + Msg("Health seems to be in bad state") + } + + logger.Debug(). + Int("code", resp.StatusCode). + Msg("Health got a good state") + + return nil + }, + } +} diff --git a/services/webfinger/pkg/command/root.go b/services/webfinger/pkg/command/root.go new file mode 100644 index 00000000000..306f33b9bd4 --- /dev/null +++ b/services/webfinger/pkg/command/root.go @@ -0,0 +1,59 @@ +package command + +import ( + "context" + "os" + + "github.com/owncloud/ocis/v2/ocis-pkg/clihelper" + ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" + "github.com/thejerf/suture/v4" + "github.com/urfave/cli/v2" +) + +// GetCommands provides all commands for this service +func GetCommands(cfg *config.Config) cli.Commands { + return []*cli.Command{ + // start this service + Server(cfg), + + // interaction with this service + + // infos about this service + Health(cfg), + Version(cfg), + } +} + +// Execute is the entry point for the ocis webfinger command. +func Execute(cfg *config.Config) error { + app := clihelper.DefaultApp(&cli.App{ + Name: "webfinger", + Usage: "Serve webfinger API for oCIS", + Commands: GetCommands(cfg), + }) + + return app.Run(os.Args) +} + +// SutureService allows for the webdav command to be embedded and supervised by a suture supervisor tree. +type SutureService struct { + cfg *config.Config +} + +// NewSutureService creates a new webdav.SutureService +func NewSutureService(cfg *ociscfg.Config) suture.Service { + cfg.Webfinger.Commons = cfg.Commons + return SutureService{ + cfg: cfg.Webfinger, + } +} + +func (s SutureService) Serve(ctx context.Context) error { + s.cfg.Context = ctx + if err := Execute(s.cfg); err != nil { + return err + } + + return nil +} diff --git a/services/webfinger/pkg/command/server.go b/services/webfinger/pkg/command/server.go new file mode 100644 index 00000000000..a7c853096d3 --- /dev/null +++ b/services/webfinger/pkg/command/server.go @@ -0,0 +1,112 @@ +package command + +import ( + "context" + "fmt" + + "github.com/oklog/run" + "github.com/owncloud/ocis/v2/ocis-pkg/config/configlog" + "github.com/owncloud/ocis/v2/ocis-pkg/version" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config/parser" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/logging" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/metrics" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/server/debug" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/server/http" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/tracing" + "github.com/urfave/cli/v2" +) + +// Server is the entrypoint for the server command. +func Server(cfg *config.Config) *cli.Command { + return &cli.Command{ + Name: "server", + Usage: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name), + Category: "server", + Before: func(c *cli.Context) error { + return configlog.ReturnFatal(parser.ParseConfig(cfg)) + }, + Action: func(c *cli.Context) error { + logger := logging.Configure(cfg.Service.Name, cfg.Log) + err := tracing.Configure(cfg) + if err != nil { + return err + } + + var ( + gr = run.Group{} + ctx, cancel = func() (context.Context, context.CancelFunc) { + if cfg.Context == nil { + return context.WithCancel(context.Background()) + } + return context.WithCancel(cfg.Context) + }() + metrics = metrics.New(metrics.Logger(logger)) + ) + + defer cancel() + + metrics.BuildInfo.WithLabelValues(version.GetString()).Set(1) + + { + svc, err := service.New(service.Logger(logger), service.Config(cfg)) + if err != nil { + logger.Error().Err(err).Msg("handler init") + return err + } + svc = service.NewInstrument(svc, metrics) + svc = service.NewLogging(svc, logger) // this logs service specific data + svc = service.NewTracing(svc) + + server, err := http.Server( + http.Logger(logger), + http.Context(ctx), + http.Config(cfg), + http.Service(svc), + ) + + if err != nil { + logger.Info(). + Err(err). + Str("transport", "http"). + Msg("Failed to initialize server") + + return err + } + + gr.Add(func() error { + return server.Run() + }, func(err error) { + logger.Error(). + Err(err). + Str("transport", "http"). + Msg("Shutting down server") + + cancel() + }) + } + + { + server, err := debug.Server( + debug.Logger(logger), + debug.Context(ctx), + debug.Config(cfg), + ) + + if err != nil { + logger.Info().Err(err).Str("transport", "debug").Msg("Failed to initialize server") + return err + } + + gr.Add(server.ListenAndServe, func(err error) { + logger.Error().Err(err) + _ = server.Shutdown(ctx) + cancel() + }) + } + + return gr.Run() + }, + } +} diff --git a/services/webfinger/pkg/command/version.go b/services/webfinger/pkg/command/version.go new file mode 100644 index 00000000000..7bc5438631c --- /dev/null +++ b/services/webfinger/pkg/command/version.go @@ -0,0 +1,50 @@ +package command + +import ( + "fmt" + "os" + + "github.com/owncloud/ocis/v2/ocis-pkg/registry" + "github.com/owncloud/ocis/v2/ocis-pkg/version" + + tw "github.com/olekukonko/tablewriter" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" + "github.com/urfave/cli/v2" +) + +// Version prints the service versions of all running instances. +func Version(cfg *config.Config) *cli.Command { + return &cli.Command{ + Name: "version", + Usage: "print the version of this binary and the running service instances", + Category: "info", + Action: func(c *cli.Context) error { + fmt.Println("Version: " + version.GetString()) + fmt.Printf("Compiled: %s\n", version.Compiled()) + fmt.Println("") + + reg := registry.GetRegistry() + services, err := reg.GetService(cfg.HTTP.Namespace + "." + cfg.Service.Name) + if err != nil { + fmt.Println(fmt.Errorf("could not get %s services from the registry: %v", cfg.Service.Name, err)) + return err + } + + if len(services) == 0 { + fmt.Println("No running " + cfg.Service.Name + " service found.") + return nil + } + + table := tw.NewWriter(os.Stdout) + table.SetHeader([]string{"Version", "Address", "Id"}) + table.SetAutoFormatHeaders(false) + for _, s := range services { + for _, n := range s.Nodes { + table.Append([]string{s.Version, n.Address, n.Id}) + } + } + table.Render() + return nil + }, + } +} diff --git a/services/webfinger/pkg/config/config.go b/services/webfinger/pkg/config/config.go new file mode 100644 index 00000000000..cdae88bd951 --- /dev/null +++ b/services/webfinger/pkg/config/config.go @@ -0,0 +1,22 @@ +package config + +import ( + "context" + + "github.com/owncloud/ocis/v2/ocis-pkg/shared" +) + +// Config combines all available configuration parts. +type Config struct { + Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service + + Service Service `yaml:"-"` + + Tracing *Tracing `yaml:"tracing"` + Log *Log `yaml:"log"` + Debug Debug `yaml:"debug"` + + HTTP HTTP `yaml:"http"` + + Context context.Context `yaml:"-"` +} diff --git a/services/webfinger/pkg/config/debug.go b/services/webfinger/pkg/config/debug.go new file mode 100644 index 00000000000..45725bdc351 --- /dev/null +++ b/services/webfinger/pkg/config/debug.go @@ -0,0 +1,9 @@ +package config + +// Debug defines the available debug configuration. +type Debug struct { + Addr string `yaml:"addr" env:"WEBFINGER_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed."` + Token string `yaml:"token" env:"WEBFINGER_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint."` + Pprof bool `yaml:"pprof" env:"WEBFINGER_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling."` + Zpages bool `yaml:"zpages" env:"WEBFINGER_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces."` +} diff --git a/services/webfinger/pkg/config/defaults/defaultconfig.go b/services/webfinger/pkg/config/defaults/defaultconfig.go new file mode 100644 index 00000000000..a74ceabe94a --- /dev/null +++ b/services/webfinger/pkg/config/defaults/defaultconfig.go @@ -0,0 +1,73 @@ +package defaults + +import ( + "strings" + + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" +) + +func FullDefaultConfig() *config.Config { + cfg := DefaultConfig() + EnsureDefaults(cfg) + Sanitize(cfg) + return cfg +} + +func DefaultConfig() *config.Config { + return &config.Config{ + Debug: config.Debug{ + Addr: "127.0.0.1:19119", // FIXME + Token: "", + Pprof: false, + Zpages: false, + }, + HTTP: config.HTTP{ + Addr: "127.0.0.1:19115", // FIXME + Root: "/", + Namespace: "com.owncloud.web", + CORS: config.CORS{ + AllowedOrigins: []string{"*"}, + }, + }, + Service: config.Service{ + Name: "webfinger", + }, + } +} + +func EnsureDefaults(cfg *config.Config) { + // provide with defaults for shared logging, since we need a valid destination address for "envdecode". + if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil { + cfg.Log = &config.Log{ + Level: cfg.Commons.Log.Level, + Pretty: cfg.Commons.Log.Pretty, + Color: cfg.Commons.Log.Color, + File: cfg.Commons.Log.File, + } + } else if cfg.Log == nil { + cfg.Log = &config.Log{} + } + // provide with defaults for shared tracing, since we need a valid destination address for "envdecode". + if cfg.Tracing == nil && cfg.Commons != nil && cfg.Commons.Tracing != nil { + cfg.Tracing = &config.Tracing{ + Enabled: cfg.Commons.Tracing.Enabled, + Type: cfg.Commons.Tracing.Type, + Endpoint: cfg.Commons.Tracing.Endpoint, + Collector: cfg.Commons.Tracing.Collector, + } + } else if cfg.Tracing == nil { + cfg.Tracing = &config.Tracing{} + } + + if cfg.Commons != nil { + cfg.HTTP.TLS = cfg.Commons.HTTPServiceTLS + } +} + +func Sanitize(cfg *config.Config) { + // sanitize config + if cfg.HTTP.Root != "/" { + cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/") + } + +} diff --git a/services/webfinger/pkg/config/http.go b/services/webfinger/pkg/config/http.go new file mode 100644 index 00000000000..0353427b61a --- /dev/null +++ b/services/webfinger/pkg/config/http.go @@ -0,0 +1,20 @@ +package config + +import "github.com/owncloud/ocis/v2/ocis-pkg/shared" + +// CORS defines the available cors configuration. +type CORS struct { + AllowedOrigins []string `yaml:"allow_origins" env:"OCIS_CORS_ALLOW_ORIGINS;WEBFINGER_CORS_ALLOW_ORIGINS" desc:"A comma-separated list of allowed CORS origins. See following chapter for more details: *Access-Control-Allow-Origin* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin"` + AllowedMethods []string `yaml:"allow_methods" env:"OCIS_CORS_ALLOW_METHODS;WEBFINGER_CORS_ALLOW_METHODS" desc:"A comma-separated list of allowed CORS methods. See following chapter for more details: *Access-Control-Request-Method* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method"` + AllowedHeaders []string `yaml:"allow_headers" env:"OCIS_CORS_ALLOW_HEADERS;WEBFINGER_CORS_ALLOW_HEADERS" desc:"A comma-separated list of allowed CORS headers. See following chapter for more details: *Access-Control-Request-Headers* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers."` + AllowCredentials bool `yaml:"allow_credentials" env:"OCIS_CORS_ALLOW_CREDENTIALS;WEBFINGER_CORS_ALLOW_CREDENTIALS" desc:"Allow credentials for CORS.See following chapter for more details: *Access-Control-Allow-Credentials* at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials."` +} + +// HTTP defines the available http configuration. +type HTTP struct { + Addr string `yaml:"addr" env:"WEBFINGER_HTTP_ADDR" desc:"The bind address of the HTTP service."` + Namespace string `yaml:"-"` + Root string `yaml:"root" env:"WEBFINGER_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service."` + CORS CORS `yaml:"cors"` + TLS shared.HTTPServiceTLS `yaml:"tls"` +} diff --git a/services/webfinger/pkg/config/log.go b/services/webfinger/pkg/config/log.go new file mode 100644 index 00000000000..036eca46f1e --- /dev/null +++ b/services/webfinger/pkg/config/log.go @@ -0,0 +1,9 @@ +package config + +// Log defines the available log configuration. +type Log struct { + Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;WEBFINGER_LOG_LEVEL" desc:"The log level. Valid values are: \"panic\", \"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\"."` + Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;WEBFINGER_LOG_PRETTY" desc:"Activates pretty log output."` + Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;WEBFINGER_LOG_COLOR" desc:"Activates colorized log output."` + File string `mapstructure:"file" env:"OCIS_LOG_FILE;WEBFINGER_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set."` +} diff --git a/services/webfinger/pkg/config/parser/parse.go b/services/webfinger/pkg/config/parser/parse.go new file mode 100644 index 00000000000..55d657e93bd --- /dev/null +++ b/services/webfinger/pkg/config/parser/parse.go @@ -0,0 +1,37 @@ +package parser + +import ( + "errors" + + ociscfg "github.com/owncloud/ocis/v2/ocis-pkg/config" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config/defaults" + + "github.com/owncloud/ocis/v2/ocis-pkg/config/envdecode" +) + +// ParseConfig loads configuration from known paths. +func ParseConfig(cfg *config.Config) error { + _, err := ociscfg.BindSourcesToStructs(cfg.Service.Name, cfg) + if err != nil { + return err + } + + defaults.EnsureDefaults(cfg) + + // load all env variables relevant to the config in the current context. + if err := envdecode.Decode(cfg); err != nil { + // no environment variable set for this config is an expected "error" + if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) { + return err + } + } + + defaults.Sanitize(cfg) + + return Validate(cfg) +} + +func Validate(cfg *config.Config) error { + return nil +} diff --git a/services/webfinger/pkg/config/service.go b/services/webfinger/pkg/config/service.go new file mode 100644 index 00000000000..d1eac383f0b --- /dev/null +++ b/services/webfinger/pkg/config/service.go @@ -0,0 +1,6 @@ +package config + +// Service defines the available service configuration. +type Service struct { + Name string `yaml:"-"` +} diff --git a/services/webfinger/pkg/config/tracing.go b/services/webfinger/pkg/config/tracing.go new file mode 100644 index 00000000000..070c77671f2 --- /dev/null +++ b/services/webfinger/pkg/config/tracing.go @@ -0,0 +1,9 @@ +package config + +// Tracing defines the available tracing configuration. +type Tracing struct { + Enabled bool `yaml:"enabled" env:"OCIS_TRACING_ENABLED;WEBFINGER_TRACING_ENABLED" desc:"Activates tracing."` + Type string `yaml:"type" env:"OCIS_TRACING_TYPE;WEBFINGER_TRACING_TYPE" desc:"The type of tracing. Defaults to \"\", which is the same as \"jaeger\". Allowed tracing types are \"jaeger\" and \"\" as of now."` + Endpoint string `yaml:"endpoint" env:"OCIS_TRACING_ENDPOINT;WEBFINGER_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent."` + Collector string `yaml:"collector" env:"OCIS_TRACING_COLLECTOR;WEBFINGER_TRACING_COLLECTOR" desc:"The HTTP endpoint for sending spans directly to a collector, i.e. http://jaeger-collector:14268/api/traces. Only used if the tracing endpoint is unset."` +} diff --git a/services/webfinger/pkg/logging/logging.go b/services/webfinger/pkg/logging/logging.go new file mode 100644 index 00000000000..cd9c29f4b1f --- /dev/null +++ b/services/webfinger/pkg/logging/logging.go @@ -0,0 +1,17 @@ +package logging + +import ( + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" +) + +// LoggerFromConfig initializes a service-specific logger instance. +func Configure(name string, cfg *config.Log) log.Logger { + return log.NewLogger( + log.Name(name), + log.Level(cfg.Level), + log.Pretty(cfg.Pretty), + log.Color(cfg.Color), + log.File(cfg.File), + ) +} diff --git a/services/webfinger/pkg/metrics/metrics.go b/services/webfinger/pkg/metrics/metrics.go new file mode 100644 index 00000000000..03707f338e5 --- /dev/null +++ b/services/webfinger/pkg/metrics/metrics.go @@ -0,0 +1,81 @@ +package metrics + +import "github.com/prometheus/client_golang/prometheus" + +var ( + // Namespace defines the namespace for the defines metrics. + Namespace = "ocis" + + // Subsystem defines the subsystem for the defines metrics. + Subsystem = "webfinger" +) + +// Metrics defines the available metrics of this service. +type Metrics struct { + BuildInfo *prometheus.GaugeVec + Counter *prometheus.CounterVec + Latency *prometheus.SummaryVec + Duration *prometheus.HistogramVec +} + +// New initializes the available metrics. +func New(opts ...Option) *Metrics { + options := newOptions(opts...) + + m := &Metrics{ + BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "build_info", + Help: "Build information", + }, []string{"version"}), + Counter: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "webfinger_total", + Help: "How many webfinger requests processed", + }, []string{}), + Latency: prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "webfinger_latency_microseconds", + Help: "Webfinger request latencies in microseconds", + }, []string{}), + Duration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "webfinger_duration_seconds", + Help: "Webfinger request time in seconds", + }, []string{}), + } + + if err := prometheus.Register(m.BuildInfo); err != nil { + options.Logger.Error(). + Err(err). + Str("metric", "BuildInfo"). + Msg("Failed to register prometheus metric") + } + + if err := prometheus.Register(m.Counter); err != nil { + options.Logger.Error(). + Err(err). + Str("metric", "counter"). + Msg("Failed to register prometheus metric") + } + + if err := prometheus.Register(m.Latency); err != nil { + options.Logger.Error(). + Err(err). + Str("metric", "latency"). + Msg("Failed to register prometheus metric") + } + + if err := prometheus.Register(m.Duration); err != nil { + options.Logger.Error(). + Err(err). + Str("metric", "duration"). + Msg("Failed to register prometheus metric") + } + + return m +} diff --git a/services/webfinger/pkg/metrics/options.go b/services/webfinger/pkg/metrics/options.go new file mode 100644 index 00000000000..4a1279c8e74 --- /dev/null +++ b/services/webfinger/pkg/metrics/options.go @@ -0,0 +1,31 @@ +package metrics + +import ( + "github.com/owncloud/ocis/v2/ocis-pkg/log" +) + +// Option defines a single option function. +type Option func(o *Options) + +// Options defines the available options for this package. +type Options struct { + Logger log.Logger +} + +// newOptions initializes the available default options. +func newOptions(opts ...Option) Options { + opt := Options{} + + for _, o := range opts { + o(&opt) + } + + return opt +} + +// Logger provides a function to set the logger option. +func Logger(val log.Logger) Option { + return func(o *Options) { + o.Logger = val + } +} diff --git a/services/webfinger/pkg/server/debug/option.go b/services/webfinger/pkg/server/debug/option.go new file mode 100644 index 00000000000..6cd6155915b --- /dev/null +++ b/services/webfinger/pkg/server/debug/option.go @@ -0,0 +1,50 @@ +package debug + +import ( + "context" + + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" +) + +// Option defines a single option function. +type Option func(o *Options) + +// Options defines the available options for this package. +type Options struct { + Logger log.Logger + Context context.Context + Config *config.Config +} + +// newOptions initializes the available default options. +func newOptions(opts ...Option) Options { + opt := Options{} + + for _, o := range opts { + o(&opt) + } + + return opt +} + +// Logger provides a function to set the logger option. +func Logger(val log.Logger) Option { + return func(o *Options) { + o.Logger = val + } +} + +// Context provides a function to set the context option. +func Context(val context.Context) Option { + return func(o *Options) { + o.Context = val + } +} + +// Config provides a function to set the config option. +func Config(val *config.Config) Option { + return func(o *Options) { + o.Config = val + } +} diff --git a/services/webfinger/pkg/server/debug/server.go b/services/webfinger/pkg/server/debug/server.go new file mode 100644 index 00000000000..b21ec5ec3f4 --- /dev/null +++ b/services/webfinger/pkg/server/debug/server.go @@ -0,0 +1,63 @@ +package debug + +import ( + "io" + "net/http" + + "github.com/owncloud/ocis/v2/ocis-pkg/service/debug" + "github.com/owncloud/ocis/v2/ocis-pkg/version" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" +) + +// Server initializes the debug service and server. +func Server(opts ...Option) (*http.Server, error) { + options := newOptions(opts...) + + return debug.NewService( + debug.Logger(options.Logger), + debug.Name(options.Config.Service.Name), + debug.Version(version.GetString()), + debug.Address(options.Config.Debug.Addr), + debug.Token(options.Config.Debug.Token), + debug.Pprof(options.Config.Debug.Pprof), + debug.Zpages(options.Config.Debug.Zpages), + debug.Health(health(options.Config)), + debug.Ready(ready(options.Config)), + debug.CorsAllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins), + debug.CorsAllowedMethods(options.Config.HTTP.CORS.AllowedMethods), + debug.CorsAllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders), + debug.CorsAllowCredentials(options.Config.HTTP.CORS.AllowCredentials), + ), nil +} + +// health implements the health check. +func health(cfg *config.Config) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + + // TODO: check if services are up and running + + _, err := io.WriteString(w, http.StatusText(http.StatusOK)) + // io.WriteString should not fail but if it does we want to know. + if err != nil { + panic(err) + } + } +} + +// ready implements the ready check. +func ready(cfg *config.Config) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + + // TODO: check if services are up and running + + _, err := io.WriteString(w, http.StatusText(http.StatusOK)) + // io.WriteString should not fail but if it does we want to know. + if err != nil { + panic(err) + } + } +} diff --git a/services/webfinger/pkg/server/http/option.go b/services/webfinger/pkg/server/http/option.go new file mode 100644 index 00000000000..28ebd40d6e2 --- /dev/null +++ b/services/webfinger/pkg/server/http/option.go @@ -0,0 +1,84 @@ +package http + +import ( + "context" + + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" + svc "github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0" + "github.com/urfave/cli/v2" +) + +// Option defines a single option function. +type Option func(o *Options) + +// Options defines the available options for this package. +type Options struct { + Name string + Namespace string + Logger log.Logger + Context context.Context + Config *config.Config + Flags []cli.Flag + Service svc.Service +} + +// newOptions initializes the available default options. +func newOptions(opts ...Option) Options { + opt := Options{} + + for _, o := range opts { + o(&opt) + } + + return opt +} + +// Name provides a name for the service. +func Name(val string) Option { + return func(o *Options) { + o.Name = val + } +} + +// Logger provides a function to set the logger option. +func Logger(val log.Logger) Option { + return func(o *Options) { + o.Logger = val + } +} + +// Context provides a function to set the context option. +func Context(val context.Context) Option { + return func(o *Options) { + o.Context = val + } +} + +// Config provides a function to set the config option. +func Config(val *config.Config) Option { + return func(o *Options) { + o.Config = val + } +} + +// Flags provides a function to set the flags option. +func Flags(val []cli.Flag) Option { + return func(o *Options) { + o.Flags = append(o.Flags, val...) + } +} + +// Namespace provides a function to set the namespace option. +func Namespace(val string) Option { + return func(o *Options) { + o.Namespace = val + } +} + +// Service provides a function to set the service option. +func Service(val svc.Service) Option { + return func(o *Options) { + o.Service = val + } +} diff --git a/services/webfinger/pkg/server/http/server.go b/services/webfinger/pkg/server/http/server.go new file mode 100644 index 00000000000..3b18c74a1e3 --- /dev/null +++ b/services/webfinger/pkg/server/http/server.go @@ -0,0 +1,98 @@ +package http + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + chimiddleware "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/render" + "github.com/owncloud/ocis/v2/ocis-pkg/cors" + "github.com/owncloud/ocis/v2/ocis-pkg/middleware" + ohttp "github.com/owncloud/ocis/v2/ocis-pkg/service/http" + "github.com/owncloud/ocis/v2/ocis-pkg/version" + "go-micro.dev/v4" +) + +// Server initializes the http service and server. +func Server(opts ...Option) (ohttp.Service, error) { + options := newOptions(opts...) + service := options.Service + + svc, err := ohttp.NewService( + ohttp.TLSConfig(options.Config.HTTP.TLS), + ohttp.Logger(options.Logger), + ohttp.Namespace(options.Config.HTTP.Namespace), + ohttp.Name(options.Config.Service.Name), + ohttp.Version(version.GetString()), + ohttp.Address(options.Config.HTTP.Addr), + ohttp.Context(options.Context), + ohttp.Flags(options.Flags...), + ) + if err != nil { + options.Logger.Error(). + Err(err). + Msg("Error initializing http service") + return ohttp.Service{}, err + } + + mux := chi.NewMux() + + mux.Use(chimiddleware.RealIP) + mux.Use(chimiddleware.RequestID) + mux.Use(middleware.TraceContext) + mux.Use(middleware.NoCache) + mux.Use( + middleware.Cors( + cors.Logger(options.Logger), + cors.AllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins), + cors.AllowedMethods(options.Config.HTTP.CORS.AllowedMethods), + cors.AllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders), + cors.AllowCredentials(options.Config.HTTP.CORS.AllowCredentials), + )) + mux.Use(middleware.Secure) + + mux.Use(middleware.Version( + options.Name, + version.String, + )) + + // this logs http request related data + mux.Use(middleware.Logger( + options.Logger, + )) + + mux.Route(options.Config.HTTP.Root, func(r chi.Router) { + + r.Get("/.well-known/webfinger", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // from https://www.rfc-editor.org/rfc/rfc7033#section-4.2 + // + // If the "resource" parameter is a value for which the server has no + // information, the server MUST indicate that it was unable to match the + // request as per Section 10.4.5 of RFC 2616. + // TODO the MUST might be a problem, is a guest instance ok enough? + resource := r.URL.Query().Get("resource") + rel := r.URL.Query().Get("rel") + + jrd, err := service.Webfinger(ctx, resource, rel) + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.PlainText(w, r, err.Error()) + return + } + + w.Header().Set("Content-type", "application/jrd+json") + render.Status(r, http.StatusOK) + render.JSON(w, r, jrd) + }) + }) + + err = micro.RegisterHandler(svc.Server(), mux) + if err != nil { + options.Logger.Fatal().Err(err).Msg("failed to register the handler") + } + + svc.Init() + return svc, nil +} diff --git a/services/webfinger/pkg/service/v0/instrument.go b/services/webfinger/pkg/service/v0/instrument.go new file mode 100644 index 00000000000..899b948891f --- /dev/null +++ b/services/webfinger/pkg/service/v0/instrument.go @@ -0,0 +1,38 @@ +package service + +import ( + "context" + + "github.com/owncloud/ocis/v2/services/webfinger/pkg/metrics" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger" + "github.com/prometheus/client_golang/prometheus" +) + +// NewInstrument returns a service that instruments metrics. +func NewInstrument(next Service, metrics *metrics.Metrics) Service { + return instrument{ + next: next, + metrics: metrics, + } +} + +type instrument struct { + next Service + metrics *metrics.Metrics +} + +// Webfinger implements the Service interface. +func (i instrument) Webfinger(ctx context.Context, resource, rel string) (webfinger.JSONResourceDescriptor, error) { + timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { + us := v * 1000000 + + i.metrics.Latency.WithLabelValues().Observe(us) + i.metrics.Duration.WithLabelValues().Observe(v) + })) + + defer timer.ObserveDuration() + + i.metrics.Counter.WithLabelValues().Inc() + + return i.next.Webfinger(ctx, resource, rel) +} diff --git a/services/webfinger/pkg/service/v0/logging.go b/services/webfinger/pkg/service/v0/logging.go new file mode 100644 index 00000000000..8f3bfde41a1 --- /dev/null +++ b/services/webfinger/pkg/service/v0/logging.go @@ -0,0 +1,31 @@ +package service + +import ( + "context" + + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger" +) + +// NewLogging returns a service that logs messages. +func NewLogging(next Service, logger log.Logger) Service { + return logging{ + next: next, + logger: logger, + } +} + +type logging struct { + next Service + logger log.Logger +} + +// Webfinger implements the Service interface. +func (l logging) Webfinger(ctx context.Context, resource, rel string) (webfinger.JSONResourceDescriptor, error) { + l.logger.Debug(). + Str("resource", resource). + Str("rel", rel). + Msg("Webfinger") + + return l.next.Webfinger(ctx, resource, rel) +} diff --git a/services/webfinger/pkg/service/v0/option.go b/services/webfinger/pkg/service/v0/option.go new file mode 100644 index 00000000000..83661244b5e --- /dev/null +++ b/services/webfinger/pkg/service/v0/option.go @@ -0,0 +1,40 @@ +package service + +import ( + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" +) + +// Option defines a single option function. +type Option func(o *Options) + +// Options defines the available options for this package. +type Options struct { + Logger log.Logger + Config *config.Config +} + +// newOptions initializes the available default options. +func newOptions(opts ...Option) Options { + opt := Options{} + + for _, o := range opts { + o(&opt) + } + + return opt +} + +// Logger provides a function to set the logger option. +func Logger(val log.Logger) Option { + return func(o *Options) { + o.Logger = val + } +} + +// Config provides a function to set the config option. +func Config(val *config.Config) Option { + return func(o *Options) { + o.Config = val + } +} diff --git a/services/webfinger/pkg/service/v0/service.go b/services/webfinger/pkg/service/v0/service.go new file mode 100644 index 00000000000..49801e46966 --- /dev/null +++ b/services/webfinger/pkg/service/v0/service.go @@ -0,0 +1,122 @@ +package service + +import ( + "context" + + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger" +) + +const ( + OwnCloudInstanceRel = "http://webfinger.owncloud/rel/server-instance" + OpenIDConnectRel = "http://openid.net/specs/connect/1.0/issuer" +) + +// Service defines the extension handlers. +type Service interface { + Webfinger(ctx context.Context, resource, rel string) (webfinger.JSONResourceDescriptor, error) +} + +// New returns a new instance of Service +func New(opts ...Option) (Service, error) { + options := newOptions(opts...) + + return svc{ + log: options.Logger, + config: options.Config, + }, nil +} + +type svc struct { + config *config.Config + log log.Logger +} + +// SpacesThumbnail is the endpoint for retrieving thumbnails inside of spaces. +// +// GET /.well-known/webfinger? +// resource=acct%3Acarol%40example.com& +// rel=http%3A%2F%2Fwebfinger.owncloud%rel%2Fserver-instance +// HTTP/1.1 +// Host: example.com +// +// The server might respond like this: +// +// HTTP/1.1 200 OK +// Access-Control-Allow-Origin: * +// Content-Type: application/jrd+json +// +// { +// "subject" : "acct:carol@example.com", +// "links" : +// [ +// { +// "rel" : "http://webfinger.owncloud/rel/server-instance", +// "href" : "https://instance.example.com", +// "titles": { +// "en": "Readable Instance Name" +// } +// }, +// { +// "rel" : "http://webfinger.owncloud/rel/server-instance", +// "href" : "https://otherinstance.example.com", +// "titles": { +// "en": "Other Readable Instance Name" +// } +// } +// ] +// } +func (s svc) Webfinger(ctx context.Context, resource, rel string) (webfinger.JSONResourceDescriptor, error) { + + // TODO query ldap server here and fetch all instances the user has access to + // what is the domain for the instance? + + // TODO use another relation? more graph specific? nah + return webfinger.JSONResourceDescriptor{ + Subject: resource, + Links: []webfinger.Link{ + { + Rel: OwnCloudInstanceRel, + Href: "https://instance.server...", + Titles: map[string]string{ + "en": "Readable Instance name", + }, + }, + { + Rel: OwnCloudInstanceRel, + Href: "https://otherinstance.server...", + Titles: map[string]string{ + "en": "Other readable Instance name", + }, + }, + // and we can return the OpenID Connect + { + Rel: OpenIDConnectRel, + Href: "https://idp.server...", + Titles: map[string]string{ + "en": "Readable Openid Connect IDP name", + }, + }, + { + Rel: OpenIDConnectRel, + Href: "https://otheridp.server...", + Titles: map[string]string{ + "en": "Other readable Openid Connect IDP name", + }, + }, + // FIXME but now the clients have no way of knowing whic idp belongs to which instance + // we could mix like this: + { + Rel: OwnCloudInstanceRel, + Href: "https://otherinstance.server...", + Titles: map[string]string{ + "en": "Other readable Instance name", + }, + Properties: map[string]string{ + OpenIDConnectRel: "https://otheridp.server...", + }, + }, + }, + }, nil +} diff --git a/services/webfinger/pkg/service/v0/tracing.go b/services/webfinger/pkg/service/v0/tracing.go new file mode 100644 index 00000000000..e36697af1cc --- /dev/null +++ b/services/webfinger/pkg/service/v0/tracing.go @@ -0,0 +1,32 @@ +package service + +import ( + "context" + + webfingertracing "github.com/owncloud/ocis/v2/services/webfinger/pkg/tracing" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// NewTracing returns a service that instruments traces. +func NewTracing(next Service) Service { + return tracing{ + next: next, + } +} + +type tracing struct { + next Service +} + +// Webfinger implements the Service interface. +func (t tracing) Webfinger(ctx context.Context, resource, rel string) (webfinger.JSONResourceDescriptor, error) { + ctx, span := webfingertracing.TraceProvider.Tracer("webfinger").Start(ctx, "Webfinger", trace.WithAttributes( + attribute.KeyValue{Key: "resource", Value: attribute.StringValue(resource)}, + attribute.KeyValue{Key: "rel", Value: attribute.StringValue(rel)}, + )) + defer span.End() + + return t.next.Webfinger(ctx, resource, rel) +} diff --git a/services/webfinger/pkg/tracing/tracing.go b/services/webfinger/pkg/tracing/tracing.go new file mode 100644 index 00000000000..95c332c5314 --- /dev/null +++ b/services/webfinger/pkg/tracing/tracing.go @@ -0,0 +1,23 @@ +package tracing + +import ( + pkgtrace "github.com/owncloud/ocis/v2/ocis-pkg/tracing" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" + "go.opentelemetry.io/otel/trace" +) + +var ( + // TraceProvider is the global trace provider for the proxy service. + TraceProvider = trace.NewNoopTracerProvider() +) + +func Configure(cfg *config.Config) error { + var err error + if cfg.Tracing.Enabled { + if TraceProvider, err = pkgtrace.GetTraceProvider(cfg.Tracing.Endpoint, cfg.Tracing.Collector, cfg.Service.Name, cfg.Tracing.Type); err != nil { + return err + } + } + + return nil +} diff --git a/services/webfinger/pkg/webfinger/webfinger.go b/services/webfinger/pkg/webfinger/webfinger.go new file mode 100644 index 00000000000..1a6f3653475 --- /dev/null +++ b/services/webfinger/pkg/webfinger/webfinger.go @@ -0,0 +1,55 @@ +package webfinger + +// Link represents a link relation object as per https://www.rfc-editor.org/rfc/rfc7033#section-4.4.4 +type Link struct { + // Rel is either a URI or a registered relation type (see RFC 5988) + // + // The "rel" member MUST be present in the link relation object. + Rel string `json:"rel"` + // Type indicates the media type of the target resource + // + // The "type" member is OPTIONAL in the link relation object. + Type string `json:"type,omitempty"` + // Href contains a URI pointing to the target resource. + // + // The "href" member is OPTIONAL in the link relation object. + Href string `json:"href,omitempty"` + + Properties map[string]string `json:"properties,omitempty"` + // Titles comprises zero or more name/value pairs whose + // names are a language tag or the string "und" + // + // Here is an example of the "titles" object: + // + // "titles" : + // { + // "en-us" : "The Magical World of Steve", + // "fr" : "Le Monde Magique de Steve" + // } + // + // The "titles" member is OPTIONAL in the link relation object. + Titles map[string]string `json:"titles,omitempty"` +} + +// JSONResourceDescriptor represents a JSON Resource Descriptor (JRD) as per https://www.rfc-editor.org/rfc/rfc7033#section-4.4 +type JSONResourceDescriptor struct { + // Subject is a URI that identifies the entity that the JRD describes + // + // The "subject" member SHOULD be present in the JRD. + Subject string `json:"subject,omitempty"` + // Aliases is an array of zero or more URI strings that identify the same + // entity as the "subject" URI. + // + // The "aliases" array is OPTIONAL in the JRD. + Aliases []string `json:"aliases,omitempty"` + // Properties is an object comprising zero or more name/value pairs whose + // names are URIs (referred to as "property identifiers") and whose + // values are strings or null. + // + // The "properties" member is OPTIONAL in the JRD. + Properties map[string]string `json:"properties,omitempty"` + // Links is an array of objects that contain link relation information + // + // The "links" array is OPTIONAL in the JRD. + Links []Link `json:"links,omitempty"` +} diff --git a/services/webfinger/reflex.conf b/services/webfinger/reflex.conf new file mode 100644 index 00000000000..32c48e4c467 --- /dev/null +++ b/services/webfinger/reflex.conf @@ -0,0 +1,2 @@ +# backend +-r '^(cmd|pkg)/.*\.go$' -R '^node_modules/' -s -- sh -c 'make bin/ocis-graph-debug && bin/ocis-graph-debug --log-level debug server --debug-pprof --debug-zpages --oidc-endpoint="https://deepdiver" --oidc-insecure=1' From d2c3f10df966c24c943961e414e08a63c4a4bb7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Wed, 11 Jan 2023 15:07:04 +0000 Subject: [PATCH 02/23] add webfinger to proxy, return current host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- .../pkg/config/defaults/defaultconfig.go | 5 ++ services/webfinger/pkg/server/http/server.go | 16 ++++ services/webfinger/pkg/service/v0/service.go | 76 +++++++++++-------- 3 files changed, 66 insertions(+), 31 deletions(-) diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index 5074dc56f05..5d1c9acfd1a 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -73,6 +73,11 @@ func DefaultPolicies() []config.Policy { Service: "com.owncloud.web.web", Unprotected: true, }, + { + Endpoint: "/.well-known/webfinger", + Service: "com.owncloud.web.webfinger", + Unprotected: true, + }, { Endpoint: "/.well-known/", Service: "com.owncloud.web.idp", diff --git a/services/webfinger/pkg/server/http/server.go b/services/webfinger/pkg/server/http/server.go index 3b18c74a1e3..ca8b968c6c6 100644 --- a/services/webfinger/pkg/server/http/server.go +++ b/services/webfinger/pkg/server/http/server.go @@ -1,6 +1,7 @@ package http import ( + "context" "net/http" "github.com/go-chi/chi/v5" @@ -66,6 +67,9 @@ func Server(opts ...Option) (ohttp.Service, error) { r.Get("/.well-known/webfinger", func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + // for now, put the url in the context so it can be used to fake a list + ctx = context.WithValue(ctx, "href", getHref(r)) + // from https://www.rfc-editor.org/rfc/rfc7033#section-4.2 // // If the "resource" parameter is a value for which the server has no @@ -96,3 +100,15 @@ func Server(opts ...Option) (ohttp.Service, error) { svc.Init() return svc, nil } + +func getHref(r *http.Request) string { + proto := r.Header.Get("x-forwarded-proto") + host := r.Header.Get("x-forwarded-host") + port := r.Header.Get("x-forwarded-port") + + if (proto == "http" && port != "80") || (proto == "https" && port != "443") { + host = host + ":" + port + } + + return proto + "://" + host +} diff --git a/services/webfinger/pkg/service/v0/service.go b/services/webfinger/pkg/service/v0/service.go index 49801e46966..25f58701c65 100644 --- a/services/webfinger/pkg/service/v0/service.go +++ b/services/webfinger/pkg/service/v0/service.go @@ -72,51 +72,65 @@ func (s svc) Webfinger(ctx context.Context, resource, rel string) (webfinger.JSO // TODO query ldap server here and fetch all instances the user has access to // what is the domain for the instance? + href := ctx.Value("href").(string) + // TODO use another relation? more graph specific? nah return webfinger.JSONResourceDescriptor{ Subject: resource, Links: []webfinger.Link{ { Rel: OwnCloudInstanceRel, - Href: "https://instance.server...", + Href: href, Titles: map[string]string{ - "en": "Readable Instance name", + "en": "ownCloud Infinite Scale", }, - }, - { - Rel: OwnCloudInstanceRel, - Href: "https://otherinstance.server...", - Titles: map[string]string{ - "en": "Other readable Instance name", + Properties: map[string]string{ + OpenIDConnectRel: href, }, }, - // and we can return the OpenID Connect - { - Rel: OpenIDConnectRel, - Href: "https://idp.server...", - Titles: map[string]string{ - "en": "Readable Openid Connect IDP name", + /* + { + Rel: OwnCloudInstanceRel, + Href: "https://instance.server...", + Titles: map[string]string{ + "en": "Readable Instance name", + }, }, - }, - { - Rel: OpenIDConnectRel, - Href: "https://otheridp.server...", - Titles: map[string]string{ - "en": "Other readable Openid Connect IDP name", + { + Rel: OwnCloudInstanceRel, + Href: "https://otherinstance.server...", + Titles: map[string]string{ + "en": "Other readable Instance name", + }, }, - }, - // FIXME but now the clients have no way of knowing whic idp belongs to which instance - // we could mix like this: - { - Rel: OwnCloudInstanceRel, - Href: "https://otherinstance.server...", - Titles: map[string]string{ - "en": "Other readable Instance name", + // and we can return the OpenID Connect + { + Rel: OpenIDConnectRel, + Href: "https://idp.server...", + Titles: map[string]string{ + "en": "Readable Openid Connect IDP name", + }, }, - Properties: map[string]string{ - OpenIDConnectRel: "https://otheridp.server...", + { + Rel: OpenIDConnectRel, + Href: "https://otheridp.server...", + Titles: map[string]string{ + "en": "Other readable Openid Connect IDP name", + }, }, - }, + // FIXME but now the clients have no way of knowing which idp belongs to which instance + // we could mix like this: + { + Rel: OwnCloudInstanceRel, + Href: "https://otherinstance.server...", + Titles: map[string]string{ + "en": "Other readable Instance name", + }, + Properties: map[string]string{ + OpenIDConnectRel: "https://otheridp.server...", + }, + }, + */ }, }, nil } From dc1508014e27073e9222edf4bbdf43cfcf8d6758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 3 Feb 2023 12:04:36 +0000 Subject: [PATCH 03/23] some cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- .../pkg/config/defaults/defaultconfig.go | 2 +- services/webfinger/README.md | 183 +++++++++++++++++- .../pkg/config/defaults/defaultconfig.go | 6 +- services/webfinger/pkg/service/v0/service.go | 44 ----- services/webfinger/pkg/webfinger/webfinger.go | 11 +- 5 files changed, 192 insertions(+), 54 deletions(-) diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index 5d1c9acfd1a..14bc159e79e 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -79,7 +79,7 @@ func DefaultPolicies() []config.Policy { Unprotected: true, }, { - Endpoint: "/.well-known/", + Endpoint: "/.well-known/openid-configuration", Service: "com.owncloud.web.idp", Unprotected: true, }, diff --git a/services/webfinger/README.md b/services/webfinger/README.md index 0742d1c0ba1..dd84b0a3026 100644 --- a/services/webfinger/README.md +++ b/services/webfinger/README.md @@ -2,12 +2,183 @@ The webfinger service provides an RFC7033 WebFinger lookup of ownCloud instances relevant for a given user account. -It is based on https://github.com/owncloud/lookup-webfinger-sciebo but also returns a `displayname` in addition to the `href` property. +It is based on https://github.com/owncloud/lookup-webfinger-sciebo but also returns localized `titles` in addition to the `href` property. -# Links -Initial issue: https://github.com/owncloud/ocis/issues/5281 +A client can discover various endpoints by making a `GET` request to `https://drive.ocis.test/.well-known/webfinger?resource=acct%3Aeinstein%40cloud.ocis.test`. -# TODO -- [ ] actually query a backend, for now ldap in context of a multi tenant deployment ... or use the graph api to list /education/schools for a user? ldap would be more direct. -- [ ] don't use the broken metrics/instrumentation/logging interface from graph / webdav services, see https://github.com/owncloud/ocis/issues/5209 +He will get a response like +``` +{ + "subject": "acct:einstein@drive.ocis.test", + "links": [ + { + "rel": "http://openid.net/specs/connect/1.0/issuer" + "href": "https://sso.example.org/cas/oidc/", + }, + { + "rel": "http://webfinger.owncloud/rel/server-instance", + "href": "https://abc.drive.example.org" + "titles": { + "en": "Readable Instance Name" + } + }, + { + "rel": "http://webfinger.owncloud/rel/server-instance", + "href": "https://xyz.drive.example.org" + "titles": { + "en": "Readable Other Instance Name" + } + }, + ] +} +``` + +In this case there are two ocis instances and the client has to ask the user which instance he wants to use. + +## TODO +Currently, clients need to make subsequent calls to: +- /status.php to check if the instance is in maintenance mode or if the version is supported +- /config.json to get the available apps for ocis web to determine which routes require authentication +- /themes/owncloud/theme.json for theming info +- /.well-known/openid-configuration, auth2 token and oidc userinfo endpoints to authenticate the user +- /ocs/v1.php/cloud/user to get the username, eg. einstein ... again? it contains the oc10 user id (marie, not the uuid) +- /ocs/v1.php/cloud/capabilities to fetch instance capabilites +- /ocs/v1.php/cloud/users/einstein to fetch the quota which could come from graph and actually is now tied to the spaces, not tu users +- /graph/v1.0/me?%24expand=memberOf to fetch the user id and the groups the user is a member of + +Here are some Ideas which need to be discussed with all client teams in the future: + +### status properties + +The /.well-known/webfinger enpdoint allows us to not only get rid of some of these calls, e.g. by embedding status.php info: + +``` +{ + "subject": "acct:einstein@drive.ocis.test", + "links": [ + { + "rel": "http://openid.net/specs/connect/1.0/issuer" + "href": "https://idp.ocis.test", + }, + { + "rel": "http://webfinger.owncloud/rel/server-instance", + "href": "https://abc.drive.ocis.test" + "titles": { + "en": "Readable Instance Name" + } + "properties": { + "http://webfinger.owncloud/prop/status/maintenance": "false", + "http://webfinger.owncloud/prop/status/version": "10.0.11.3" + } + }, + { + "rel": "http://webfinger.owncloud/rel/server-instance", + "href": "https://xyz.drive.ocis.test" + "titles": { + "en": "Readable Other Instance Name" + } + "properties": { + "http://webfinger.owncloud/prop/status/maintenance": "true", + "http://webfinger.owncloud/prop/status/version": "10.0.11.2" + } + }, + ] +} +``` + +### Dedicated ocis web endpoint + +It also allows us to move some services out of a sharded deployment. We could e.g. introduce a relation for a common ocis web endpoint to not exponse the different instances in the browser bar: +``` +{ + "subject": "acct:einstein@drive.ocis.test", + "links": [ + { + "rel": "http://openid.net/specs/connect/1.0/issuer" + "href": "https://idp.ocis.test", + }, + { + "rel": "http://webfinger.owncloud/rel/web" + "href": "https://drive.ocis.test", + }, + { + "rel": "http://webfinger.owncloud/rel/server-instance", + "href": "https://abc.drive.ocis.test" + "titles": { + "en": "Readable Instance Name" + } + }, + { + "rel": "http://webfinger.owncloud/rel/server-instance", + "href": "https://xyz.drive.ocis.test" + "titles": { + "en": "Readable Other Instance Name" + } + }, + ] +} +``` + +### Dedicated ocis web endpoint + +We could also omit the `http://webfinger.owncloud/rel/server-instance` relation and go straight for a graph service with e.g. `rel=http://libregraph.org/rel/graph`: +``` +{ + "subject": "acct:einstein@drive.ocis.test", + "links": [ + { + "rel": "http://openid.net/specs/connect/1.0/issuer" + "href": "https://idp.ocis.test", + }, + { + "rel": "http://webfinger.owncloud/rel/web" + "href": "https://drive.ocis.test", + }, + { + "rel": "http://libregraph.org/rel/graph", + "href": "https://abc.drive.ocis.test/graph/v1.0" + "titles": { + "en": "Readable Instance Name" + } + }, + ] +} +``` + +In theory the graph endpoint would allow discovering drives on any domain. But there is a lot more work to be done here. + +### Subject properties + +We could also embed subject metadata, however since apps like ocis web also need the groups a user is member of a dadicated call to the libregraph api is probably better. In any case, we could return properties for the subject: +``` +{ + "subject": "acct:einstein@drive.ocis.test", + "properties": { + "http://libregraph.org/prop/user/id": "4c510ada-c86b-4815-8820-42cdf82c3d51", + "http://libregraph.org/prop/user/onPremisesSamAccountName": "einstein", + "http://libregraph.org/prop/user/mail": "einstein@example.org", + "http://libregraph.org/prop/user/displayName": "Albert Einstein", + } + "links": [ + { + "rel": "http://openid.net/specs/connect/1.0/issuer" + "href": "https://idp.ocis.test", + }, + { + "rel": "http://webfinger.owncloud/rel/server-instance", + "href": "https://abc.drive.ocis.test" + "titles": { + "en": "Readable Instance Name" + } + }, + { + "rel": "http://webfinger.owncloud/rel/server-instance", + "href": "https://xyz.drive.ocis.test" + "titles": { + "en": "Readable Other Instance Name" + } + }, + ] +} +``` diff --git a/services/webfinger/pkg/config/defaults/defaultconfig.go b/services/webfinger/pkg/config/defaults/defaultconfig.go index a74ceabe94a..5d53078f170 100644 --- a/services/webfinger/pkg/config/defaults/defaultconfig.go +++ b/services/webfinger/pkg/config/defaults/defaultconfig.go @@ -16,13 +16,15 @@ func FullDefaultConfig() *config.Config { func DefaultConfig() *config.Config { return &config.Config{ Debug: config.Debug{ - Addr: "127.0.0.1:19119", // FIXME + Addr: "127.0.0.1:19119", // FIXME + //Addr: "127.0.0.1:0", // :0 to pick any free local port Token: "", Pprof: false, Zpages: false, }, HTTP: config.HTTP{ - Addr: "127.0.0.1:19115", // FIXME + Addr: "127.0.0.1:19115", // FIXME + //Addr: "127.0.0.1:0", // :0 to pick any free local port Root: "/", Namespace: "com.owncloud.web", CORS: config.CORS{ diff --git a/services/webfinger/pkg/service/v0/service.go b/services/webfinger/pkg/service/v0/service.go index 25f58701c65..75e0577a96c 100644 --- a/services/webfinger/pkg/service/v0/service.go +++ b/services/webfinger/pkg/service/v0/service.go @@ -74,7 +74,6 @@ func (s svc) Webfinger(ctx context.Context, resource, rel string) (webfinger.JSO href := ctx.Value("href").(string) - // TODO use another relation? more graph specific? nah return webfinger.JSONResourceDescriptor{ Subject: resource, Links: []webfinger.Link{ @@ -88,49 +87,6 @@ func (s svc) Webfinger(ctx context.Context, resource, rel string) (webfinger.JSO OpenIDConnectRel: href, }, }, - /* - { - Rel: OwnCloudInstanceRel, - Href: "https://instance.server...", - Titles: map[string]string{ - "en": "Readable Instance name", - }, - }, - { - Rel: OwnCloudInstanceRel, - Href: "https://otherinstance.server...", - Titles: map[string]string{ - "en": "Other readable Instance name", - }, - }, - // and we can return the OpenID Connect - { - Rel: OpenIDConnectRel, - Href: "https://idp.server...", - Titles: map[string]string{ - "en": "Readable Openid Connect IDP name", - }, - }, - { - Rel: OpenIDConnectRel, - Href: "https://otheridp.server...", - Titles: map[string]string{ - "en": "Other readable Openid Connect IDP name", - }, - }, - // FIXME but now the clients have no way of knowing which idp belongs to which instance - // we could mix like this: - { - Rel: OwnCloudInstanceRel, - Href: "https://otherinstance.server...", - Titles: map[string]string{ - "en": "Other readable Instance name", - }, - Properties: map[string]string{ - OpenIDConnectRel: "https://otheridp.server...", - }, - }, - */ }, }, nil } diff --git a/services/webfinger/pkg/webfinger/webfinger.go b/services/webfinger/pkg/webfinger/webfinger.go index 1a6f3653475..f4674376ea7 100644 --- a/services/webfinger/pkg/webfinger/webfinger.go +++ b/services/webfinger/pkg/webfinger/webfinger.go @@ -14,7 +14,16 @@ type Link struct { // // The "href" member is OPTIONAL in the link relation object. Href string `json:"href,omitempty"` - + // The "properties" object within the link relation object comprises + // zero or more name/value pairs whose names are URIs (referred to as + // "property identifiers") and whose values are strings or null. + // + // Properties are used to convey additional information about the link + // relation. As an example, consider this use of "properties": + // + // "properties" : { "http://webfinger.example/mail/port" : "993" } + // + // The "properties" member is OPTIONAL in the link relation object. Properties map[string]string `json:"properties,omitempty"` // Titles comprises zero or more name/value pairs whose // names are a language tag or the string "und" From 5d70f0dbf888a6dd1f8b8e57d27a867013e3c286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 3 Feb 2023 14:37:46 +0000 Subject: [PATCH 04/23] allow passing multiple rel params MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- services/webfinger/pkg/config/config.go | 3 +++ services/webfinger/pkg/server/http/server.go | 9 +++++++-- services/webfinger/pkg/service/v0/instrument.go | 4 ++-- services/webfinger/pkg/service/v0/logging.go | 6 +++--- services/webfinger/pkg/service/v0/service.go | 16 +++++++++++----- services/webfinger/pkg/service/v0/tracing.go | 6 +++--- 6 files changed, 29 insertions(+), 15 deletions(-) diff --git a/services/webfinger/pkg/config/config.go b/services/webfinger/pkg/config/config.go index cdae88bd951..33f2e8f5824 100644 --- a/services/webfinger/pkg/config/config.go +++ b/services/webfinger/pkg/config/config.go @@ -18,5 +18,8 @@ type Config struct { HTTP HTTP `yaml:"http"` + Rules string `yaml:"webdav_namespace" env:"WEBFINGER_" desc:"Jail requests to /dav/webdav into this CS3 namespace. Supports template layouting with CS3 User properties."` + // TODO wie proxy? + Context context.Context `yaml:"-"` } diff --git a/services/webfinger/pkg/server/http/server.go b/services/webfinger/pkg/server/http/server.go index ca8b968c6c6..bb3c375f885 100644 --- a/services/webfinger/pkg/server/http/server.go +++ b/services/webfinger/pkg/server/http/server.go @@ -77,9 +77,14 @@ func Server(opts ...Option) (ohttp.Service, error) { // request as per Section 10.4.5 of RFC 2616. // TODO the MUST might be a problem, is a guest instance ok enough? resource := r.URL.Query().Get("resource") - rel := r.URL.Query().Get("rel") + rels := make([]string, 0) + for k, v := range r.URL.Query() { + if k == "rel" { + rels = append(rels, v...) + } + } - jrd, err := service.Webfinger(ctx, resource, rel) + jrd, err := service.Webfinger(ctx, resource, rels) if err != nil { render.Status(r, http.StatusInternalServerError) render.PlainText(w, r, err.Error()) diff --git a/services/webfinger/pkg/service/v0/instrument.go b/services/webfinger/pkg/service/v0/instrument.go index 899b948891f..0894620fe6c 100644 --- a/services/webfinger/pkg/service/v0/instrument.go +++ b/services/webfinger/pkg/service/v0/instrument.go @@ -22,7 +22,7 @@ type instrument struct { } // Webfinger implements the Service interface. -func (i instrument) Webfinger(ctx context.Context, resource, rel string) (webfinger.JSONResourceDescriptor, error) { +func (i instrument) Webfinger(ctx context.Context, resource string, rels []string) (webfinger.JSONResourceDescriptor, error) { timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { us := v * 1000000 @@ -34,5 +34,5 @@ func (i instrument) Webfinger(ctx context.Context, resource, rel string) (webfin i.metrics.Counter.WithLabelValues().Inc() - return i.next.Webfinger(ctx, resource, rel) + return i.next.Webfinger(ctx, resource, rels) } diff --git a/services/webfinger/pkg/service/v0/logging.go b/services/webfinger/pkg/service/v0/logging.go index 8f3bfde41a1..5d280f1c1ce 100644 --- a/services/webfinger/pkg/service/v0/logging.go +++ b/services/webfinger/pkg/service/v0/logging.go @@ -21,11 +21,11 @@ type logging struct { } // Webfinger implements the Service interface. -func (l logging) Webfinger(ctx context.Context, resource, rel string) (webfinger.JSONResourceDescriptor, error) { +func (l logging) Webfinger(ctx context.Context, resource string, rels []string) (webfinger.JSONResourceDescriptor, error) { l.logger.Debug(). Str("resource", resource). - Str("rel", rel). + Strs("rel", rels). Msg("Webfinger") - return l.next.Webfinger(ctx, resource, rel) + return l.next.Webfinger(ctx, resource, rels) } diff --git a/services/webfinger/pkg/service/v0/service.go b/services/webfinger/pkg/service/v0/service.go index 75e0577a96c..6465410ef57 100644 --- a/services/webfinger/pkg/service/v0/service.go +++ b/services/webfinger/pkg/service/v0/service.go @@ -15,7 +15,7 @@ const ( // Service defines the extension handlers. type Service interface { - Webfinger(ctx context.Context, resource, rel string) (webfinger.JSONResourceDescriptor, error) + Webfinger(ctx context.Context, resource string, rels []string) (webfinger.JSONResourceDescriptor, error) } // New returns a new instance of Service @@ -67,25 +67,31 @@ type svc struct { // } // ] // } -func (s svc) Webfinger(ctx context.Context, resource, rel string) (webfinger.JSONResourceDescriptor, error) { +func (s svc) Webfinger(ctx context.Context, resource string, rel []string) (webfinger.JSONResourceDescriptor, error) { // TODO query ldap server here and fetch all instances the user has access to // what is the domain for the instance? + // or fetch from claims + href := ctx.Value("href").(string) return webfinger.JSONResourceDescriptor{ Subject: resource, Links: []webfinger.Link{ + { + Rel: OpenIDConnectRel, + Href: href, + Titles: map[string]string{ + "en": "ownCloud Infinite Scale OpenID Connect Identity Provider", + }, + }, { Rel: OwnCloudInstanceRel, Href: href, Titles: map[string]string{ "en": "ownCloud Infinite Scale", }, - Properties: map[string]string{ - OpenIDConnectRel: href, - }, }, }, }, nil diff --git a/services/webfinger/pkg/service/v0/tracing.go b/services/webfinger/pkg/service/v0/tracing.go index e36697af1cc..6d3cc9ea267 100644 --- a/services/webfinger/pkg/service/v0/tracing.go +++ b/services/webfinger/pkg/service/v0/tracing.go @@ -21,12 +21,12 @@ type tracing struct { } // Webfinger implements the Service interface. -func (t tracing) Webfinger(ctx context.Context, resource, rel string) (webfinger.JSONResourceDescriptor, error) { +func (t tracing) Webfinger(ctx context.Context, resource string, rels []string) (webfinger.JSONResourceDescriptor, error) { ctx, span := webfingertracing.TraceProvider.Tracer("webfinger").Start(ctx, "Webfinger", trace.WithAttributes( attribute.KeyValue{Key: "resource", Value: attribute.StringValue(resource)}, - attribute.KeyValue{Key: "rel", Value: attribute.StringValue(rel)}, + attribute.KeyValue{Key: "rels", Value: attribute.StringSliceValue(rels)}, )) defer span.End() - return t.next.Webfinger(ctx, resource, rel) + return t.next.Webfinger(ctx, resource, rels) } From 9a8cd17a02acb2528d7a1605f9629b4339169faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Sat, 4 Feb 2023 15:46:13 +0000 Subject: [PATCH 05/23] introduce interfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- services/webfinger/pkg/command/server.go | 24 ++- services/webfinger/pkg/config/config.go | 3 + services/webfinger/pkg/server/http/server.go | 34 +++- services/webfinger/pkg/service/v0/errors.go | 5 + .../webfinger/pkg/service/v0/instrument.go | 5 +- services/webfinger/pkg/service/v0/logging.go | 7 +- services/webfinger/pkg/service/v0/option.go | 18 +- services/webfinger/pkg/service/v0/service.go | 187 ++++++++++++------ services/webfinger/pkg/service/v0/tracing.go | 7 +- 9 files changed, 210 insertions(+), 80 deletions(-) create mode 100644 services/webfinger/pkg/service/v0/errors.go diff --git a/services/webfinger/pkg/command/server.go b/services/webfinger/pkg/command/server.go index a7c853096d3..9bbe1dd67f2 100644 --- a/services/webfinger/pkg/command/server.go +++ b/services/webfinger/pkg/command/server.go @@ -50,7 +50,15 @@ func Server(cfg *config.Config) *cli.Command { metrics.BuildInfo.WithLabelValues(version.GetString()).Set(1) { - svc, err := service.New(service.Logger(logger), service.Config(cfg)) + svc, err := service.New( + service.Logger(logger), + service.Config(cfg), + service.WithInstanceSelector(getInstanceSelector(cfg.InstanceSelector)), + service.WithInstanceLookup(getInstanceLookup(cfg.InstanceLookup)), + // TODO pass in InstanceSelector + // TODO pass in InsanceLookup + + ) if err != nil { logger.Error().Err(err).Msg("handler init") return err @@ -110,3 +118,17 @@ func Server(cfg *config.Config) *cli.Command { }, } } + +func getInstanceSelector(selector string) service.InstanceSelector { + switch selector { + default: + return service.DefaultInstanceSelector{} + } +} + +func getInstanceLookup(lookup string) service.InstanceLookup { + switch lookup { + default: + return service.DefaultInstanceLookup{} + } +} diff --git a/services/webfinger/pkg/config/config.go b/services/webfinger/pkg/config/config.go index 33f2e8f5824..16affe60974 100644 --- a/services/webfinger/pkg/config/config.go +++ b/services/webfinger/pkg/config/config.go @@ -18,6 +18,9 @@ type Config struct { HTTP HTTP `yaml:"http"` + InstanceSelector string `yaml:"instance_selector" env:"WEBFINGER_INSTANCE_SELECTOR" desc:"How to select which instance to use for an account. Can be 'default', 'regex' or 'claims'?"` + InstanceLookup string `yaml:"instance_lookup" env:"WEBFINGER_INSTANCE_LOOKUP" desc:"How to look up to instance href and topic. Can be 'default', 'template', 'static' or 'ldap'?"` + Rules string `yaml:"webdav_namespace" env:"WEBFINGER_" desc:"Jail requests to /dav/webdav into this CS3 namespace. Supports template layouting with CS3 User properties."` // TODO wie proxy? diff --git a/services/webfinger/pkg/server/http/server.go b/services/webfinger/pkg/server/http/server.go index bb3c375f885..090dd785ce1 100644 --- a/services/webfinger/pkg/server/http/server.go +++ b/services/webfinger/pkg/server/http/server.go @@ -2,7 +2,9 @@ package http import ( "context" + "errors" "net/http" + "net/url" "github.com/go-chi/chi/v5" chimiddleware "github.com/go-chi/chi/v5/middleware" @@ -11,6 +13,7 @@ import ( "github.com/owncloud/ocis/v2/ocis-pkg/middleware" ohttp "github.com/owncloud/ocis/v2/ocis-pkg/service/http" "github.com/owncloud/ocis/v2/ocis-pkg/version" + serviceErrors "github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0" "go-micro.dev/v4" ) @@ -70,13 +73,20 @@ func Server(opts ...Option) (ohttp.Service, error) { // for now, put the url in the context so it can be used to fake a list ctx = context.WithValue(ctx, "href", getHref(r)) - // from https://www.rfc-editor.org/rfc/rfc7033#section-4.2 - // - // If the "resource" parameter is a value for which the server has no - // information, the server MUST indicate that it was unable to match the - // request as per Section 10.4.5 of RFC 2616. - // TODO the MUST might be a problem, is a guest instance ok enough? + // A WebFinger URI MUST contain a query component (see Section 3.4 of + // RFC 3986). The query component MUST contain a "resource" parameter + // and MAY contain one or more "rel" parameters. resource := r.URL.Query().Get("resource") + queryTarget, err := url.Parse(resource) + if resource == "" || err != nil { + // If the "resource" parameter is absent or malformed, the WebFinger + // resource MUST indicate that the request is bad as per Section 10.4.1 + // of RFC 2616. + render.Status(r, http.StatusBadRequest) + render.PlainText(w, r, "absent or malformed 'resource' parameter") + return + } + rels := make([]string, 0) for k, v := range r.URL.Query() { if k == "rel" { @@ -84,7 +94,17 @@ func Server(opts ...Option) (ohttp.Service, error) { } } - jrd, err := service.Webfinger(ctx, resource, rels) + jrd, err := service.Webfinger(ctx, queryTarget, rels) + if errors.Is(err, serviceErrors.ErrNotFound) { + // from https://www.rfc-editor.org/rfc/rfc7033#section-4.2 + // + // If the "resource" parameter is a value for which the server has no + // information, the server MUST indicate that it was unable to match the + // request as per Section 10.4.5 of RFC 2616. + render.Status(r, http.StatusNotFound) + render.PlainText(w, r, err.Error()) + return + } if err != nil { render.Status(r, http.StatusInternalServerError) render.PlainText(w, r, err.Error()) diff --git a/services/webfinger/pkg/service/v0/errors.go b/services/webfinger/pkg/service/v0/errors.go new file mode 100644 index 00000000000..b71ec4ca73f --- /dev/null +++ b/services/webfinger/pkg/service/v0/errors.go @@ -0,0 +1,5 @@ +package service + +import "errors" + +var ErrNotFound = errors.New("query target not found") diff --git a/services/webfinger/pkg/service/v0/instrument.go b/services/webfinger/pkg/service/v0/instrument.go index 0894620fe6c..47f40d7a138 100644 --- a/services/webfinger/pkg/service/v0/instrument.go +++ b/services/webfinger/pkg/service/v0/instrument.go @@ -2,6 +2,7 @@ package service import ( "context" + "net/url" "github.com/owncloud/ocis/v2/services/webfinger/pkg/metrics" "github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger" @@ -22,7 +23,7 @@ type instrument struct { } // Webfinger implements the Service interface. -func (i instrument) Webfinger(ctx context.Context, resource string, rels []string) (webfinger.JSONResourceDescriptor, error) { +func (i instrument) Webfinger(ctx context.Context, queryTarget *url.URL, rels []string) (webfinger.JSONResourceDescriptor, error) { timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { us := v * 1000000 @@ -34,5 +35,5 @@ func (i instrument) Webfinger(ctx context.Context, resource string, rels []strin i.metrics.Counter.WithLabelValues().Inc() - return i.next.Webfinger(ctx, resource, rels) + return i.next.Webfinger(ctx, queryTarget, rels) } diff --git a/services/webfinger/pkg/service/v0/logging.go b/services/webfinger/pkg/service/v0/logging.go index 5d280f1c1ce..5fd932bd2f6 100644 --- a/services/webfinger/pkg/service/v0/logging.go +++ b/services/webfinger/pkg/service/v0/logging.go @@ -2,6 +2,7 @@ package service import ( "context" + "net/url" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger" @@ -21,11 +22,11 @@ type logging struct { } // Webfinger implements the Service interface. -func (l logging) Webfinger(ctx context.Context, resource string, rels []string) (webfinger.JSONResourceDescriptor, error) { +func (l logging) Webfinger(ctx context.Context, queryTarget *url.URL, rels []string) (webfinger.JSONResourceDescriptor, error) { l.logger.Debug(). - Str("resource", resource). + Str("query_target", queryTarget.String()). Strs("rel", rels). Msg("Webfinger") - return l.next.Webfinger(ctx, resource, rels) + return l.next.Webfinger(ctx, queryTarget, rels) } diff --git a/services/webfinger/pkg/service/v0/option.go b/services/webfinger/pkg/service/v0/option.go index 83661244b5e..3a93a7eb943 100644 --- a/services/webfinger/pkg/service/v0/option.go +++ b/services/webfinger/pkg/service/v0/option.go @@ -10,8 +10,10 @@ type Option func(o *Options) // Options defines the available options for this package. type Options struct { - Logger log.Logger - Config *config.Config + Logger log.Logger + Config *config.Config + InstanceIdSelector InstanceSelector + InstanceLookup InstanceLookup } // newOptions initializes the available default options. @@ -38,3 +40,15 @@ func Config(val *config.Config) Option { o.Config = val } } + +func WithInstanceSelector(val InstanceSelector) Option { + return func(o *Options) { + o.InstanceIdSelector = val + } +} + +func WithInstanceLookup(val InstanceLookup) Option { + return func(o *Options) { + o.InstanceLookup = val + } +} diff --git a/services/webfinger/pkg/service/v0/service.go b/services/webfinger/pkg/service/v0/service.go index 6465410ef57..3e985dc3066 100644 --- a/services/webfinger/pkg/service/v0/service.go +++ b/services/webfinger/pkg/service/v0/service.go @@ -2,6 +2,8 @@ package service import ( "context" + "net/url" + "strings" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" @@ -15,84 +17,145 @@ const ( // Service defines the extension handlers. type Service interface { - Webfinger(ctx context.Context, resource string, rels []string) (webfinger.JSONResourceDescriptor, error) + // Webfinger is the endpoint for retrieving various href relations. + // + // GET /.well-known/webfinger? + // resource=acct%3Acarol%40example.com& + // rel=http%3A%2F%2Fwebfinger.owncloud%rel%2Fserver-instance + // HTTP/1.1 + // Host: example.com + // + // The server might respond like this: + // + // HTTP/1.1 200 OK + // Access-Control-Allow-Origin: * + // Content-Type: application/jrd+json + // + // { + // "subject" : "acct:carol@example.com", + // "links" : + // [ + // { + // "rel" : "http://webfinger.owncloud/rel/server-instance", + // "href" : "https://instance.example.com", + // "titles": { + // "en": "Readable Instance Name" + // } + // }, + // { + // "rel" : "http://webfinger.owncloud/rel/server-instance", + // "href" : "https://otherinstance.example.com", + // "titles": { + // "en": "Other Readable Instance Name" + // } + // } + // ] + // } + Webfinger(ctx context.Context, queryTarget *url.URL, rels []string) (webfinger.JSONResourceDescriptor, error) +} + +type InstanceSelector interface { + GetInstanceIds(ctx context.Context, account string) []string +} + +type InstanceLookup interface { + GetInstance(ctx context.Context, id string) Instance + // get multiple instances at once? +} + +type Instance struct { + Href string + Titles map[string]string +} + +type DefaultInstanceSelector struct{} + +func (s DefaultInstanceSelector) GetInstanceIds(ctx context.Context, account string) []string { + return []string{"default"} +} + +type DefaultInstanceLookup struct{} + +func (l DefaultInstanceLookup) GetInstance(ctx context.Context, id string) Instance { + if id == "default" { + return Instance{ + Href: ctx.Value("href").(string), + Titles: map[string]string{ + "en": "ownCloud Infinite Scale", + }, + } + } + return Instance{} } // New returns a new instance of Service func New(opts ...Option) (Service, error) { options := newOptions(opts...) + if options.InstanceIdSelector == nil { + options.InstanceIdSelector = DefaultInstanceSelector{} + } + if options.InstanceLookup == nil { + options.InstanceLookup = DefaultInstanceLookup{} + } + // TODO use fallback implementations of InstanceIdLookup and InstanceLookup? + // The InstanceIdLookup may have to happen earlier? + return svc{ - log: options.Logger, - config: options.Config, + log: options.Logger, + config: options.Config, + instanceIdSelector: options.InstanceIdSelector, + instanceLookup: options.InstanceLookup, }, nil } type svc struct { - config *config.Config - log log.Logger + config *config.Config + log log.Logger + instanceIdSelector InstanceSelector + instanceLookup InstanceLookup } -// SpacesThumbnail is the endpoint for retrieving thumbnails inside of spaces. -// -// GET /.well-known/webfinger? -// resource=acct%3Acarol%40example.com& -// rel=http%3A%2F%2Fwebfinger.owncloud%rel%2Fserver-instance -// HTTP/1.1 -// Host: example.com -// -// The server might respond like this: -// -// HTTP/1.1 200 OK -// Access-Control-Allow-Origin: * -// Content-Type: application/jrd+json -// -// { -// "subject" : "acct:carol@example.com", -// "links" : -// [ -// { -// "rel" : "http://webfinger.owncloud/rel/server-instance", -// "href" : "https://instance.example.com", -// "titles": { -// "en": "Readable Instance Name" -// } -// }, -// { -// "rel" : "http://webfinger.owncloud/rel/server-instance", -// "href" : "https://otherinstance.example.com", -// "titles": { -// "en": "Other Readable Instance Name" -// } -// } -// ] -// } -func (s svc) Webfinger(ctx context.Context, resource string, rel []string) (webfinger.JSONResourceDescriptor, error) { - - // TODO query ldap server here and fetch all instances the user has access to - // what is the domain for the instance? - - // or fetch from claims +// TODO implement different implementations: +// static one returning the href or a configureable domain +// regex one returning different instances based on the regex that matches +// claim one that reads a claim and then fetches the instance? +// that is actually two interfaces / steps: +// - one that determines the instances/schools id (read from claim, regex match) +// - one that looks up in instance by id (use template, read from json, read from ldap, read from graph) + +// Webfinger implements the service interface +func (s svc) Webfinger(ctx context.Context, queryTarget *url.URL, rel []string) (webfinger.JSONResourceDescriptor, error) { + + if queryTarget.Scheme != "acct" { + // wa can only handle acct lookup + return webfinger.JSONResourceDescriptor{}, ErrNotFound + } + + instanceIds := s.instanceIdSelector.GetInstanceIds(ctx, strings.TrimPrefix(queryTarget.String(), "acct:")) href := ctx.Value("href").(string) - return webfinger.JSONResourceDescriptor{ - Subject: resource, - Links: []webfinger.Link{ - { - Rel: OpenIDConnectRel, - Href: href, - Titles: map[string]string{ - "en": "ownCloud Infinite Scale OpenID Connect Identity Provider", - }, - }, - { - Rel: OwnCloudInstanceRel, - Href: href, - Titles: map[string]string{ - "en": "ownCloud Infinite Scale", - }, - }, + links := make([]webfinger.Link, 0, len(instanceIds)) + // TODO, make listing oidc configuration optional + links = append(links, webfinger.Link{ + Rel: OpenIDConnectRel, + Href: href, + Titles: map[string]string{ + "en": "ownCloud Infinite Scale OpenID Connect Identity Provider", }, + }) + for _, instanceId := range instanceIds { + instance := s.instanceLookup.GetInstance(ctx, instanceId) + links = append(links, webfinger.Link{ + Rel: OwnCloudInstanceRel, + Href: instance.Href, + Titles: instance.Titles, + }) + } + + return webfinger.JSONResourceDescriptor{ + Subject: queryTarget.String(), + Links: links, }, nil } diff --git a/services/webfinger/pkg/service/v0/tracing.go b/services/webfinger/pkg/service/v0/tracing.go index 6d3cc9ea267..43dc863abdb 100644 --- a/services/webfinger/pkg/service/v0/tracing.go +++ b/services/webfinger/pkg/service/v0/tracing.go @@ -2,6 +2,7 @@ package service import ( "context" + "net/url" webfingertracing "github.com/owncloud/ocis/v2/services/webfinger/pkg/tracing" "github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger" @@ -21,12 +22,12 @@ type tracing struct { } // Webfinger implements the Service interface. -func (t tracing) Webfinger(ctx context.Context, resource string, rels []string) (webfinger.JSONResourceDescriptor, error) { +func (t tracing) Webfinger(ctx context.Context, queryTarget *url.URL, rels []string) (webfinger.JSONResourceDescriptor, error) { ctx, span := webfingertracing.TraceProvider.Tracer("webfinger").Start(ctx, "Webfinger", trace.WithAttributes( - attribute.KeyValue{Key: "resource", Value: attribute.StringValue(resource)}, + attribute.KeyValue{Key: "query_target", Value: attribute.StringValue(queryTarget.String())}, attribute.KeyValue{Key: "rels", Value: attribute.StringSliceValue(rels)}, )) defer span.End() - return t.next.Webfinger(ctx, resource, rels) + return t.next.Webfinger(ctx, queryTarget, rels) } From 106d98530284c84e77e5d2bf678a88022dea431c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Tue, 7 Feb 2023 21:01:12 +0000 Subject: [PATCH 06/23] parse oidc auth token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- docs/services/wellknown/_index.md | 24 ++++ ocis-pkg/middleware/oidc.go | 129 ++++++++++++++++++ ocis-pkg/middleware/options.go | 31 +++++ services/webfinger/README.md | 24 ++++ services/webfinger/pkg/command/server.go | 38 ++++-- services/webfinger/pkg/config/config.go | 20 ++- .../pkg/config/defaults/defaultconfig.go | 32 +++++ services/webfinger/pkg/relations/noop.go | 15 ++ .../pkg/relations/openid-discovery.go | 43 ++++++ .../pkg/relations/owncloud-account.go | 57 ++++++++ .../pkg/relations/owncloud-instance.go | 67 +++++++++ services/webfinger/pkg/server/http/server.go | 12 +- services/webfinger/pkg/service/v0/option.go | 17 +-- services/webfinger/pkg/service/v0/service.go | 94 +++++++------ 14 files changed, 530 insertions(+), 73 deletions(-) create mode 100644 docs/services/wellknown/_index.md create mode 100644 ocis-pkg/middleware/oidc.go create mode 100644 ocis-pkg/middleware/options.go create mode 100644 services/webfinger/pkg/relations/noop.go create mode 100644 services/webfinger/pkg/relations/openid-discovery.go create mode 100644 services/webfinger/pkg/relations/owncloud-account.go create mode 100644 services/webfinger/pkg/relations/owncloud-instance.go diff --git a/docs/services/wellknown/_index.md b/docs/services/wellknown/_index.md new file mode 100644 index 00000000000..ec89fbd88c7 --- /dev/null +++ b/docs/services/wellknown/_index.md @@ -0,0 +1,24 @@ +--- +title: Well-Known +date: 2023-02-03T00:00:00+00:00 +weight: 20 +geekdocRepo: https://github.com/owncloud/ocis +geekdocEditPath: edit/master/docs/services/well-known +geekdocFilePath: _index.md +geekdocCollapseSection: true +--- + +## Abstract + +This service provides endpoints on the /.well-known API + +## Table of Contents + +{{< toc-tree >}} + + +## Webfinger + +## oCIS-configuration + +## Libregraph? diff --git a/ocis-pkg/middleware/oidc.go b/ocis-pkg/middleware/oidc.go new file mode 100644 index 00000000000..d29b1793477 --- /dev/null +++ b/ocis-pkg/middleware/oidc.go @@ -0,0 +1,129 @@ +package middleware + +import ( + "encoding/json" + "io" + "net/http" + "strings" + "sync" + "time" + + "github.com/MicahParks/keyfunc" + "github.com/golang-jwt/jwt/v4" + "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/ocis-pkg/oidc" + "github.com/owncloud/ocis/v2/services/proxy/pkg/config" +) + +// newOidcOptions initializes the available default options. +func newOidcOptions(opts ...Option) Options { + opt := Options{} + + for _, o := range opts { + o(&opt) + } + + return opt +} + +// OidcAuth provides a middleware to authenticate a bearer auth with an OpenID Connect identity provider +// It will put all claims provided by the userinfo endpoint in the context +func OidcAuth(opts ...Option) func(http.Handler) http.Handler { + opt := newOidcOptions(opts...) + + // TODO use a micro store cache option + + var JWKS *keyfunc.JWKS + getKeyfuncOnce := sync.Once{} + issuer := "https://cloud.ocis.test" + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + authHeader := r.Header.Get("Authorization") + switch { + case strings.HasPrefix(authHeader, "Bearer "): + getKeyfuncOnce.Do(func() { + JWKS = getKeyfunc(opt.Logger, issuer, &http.Client{}, config.JWKS{ + RefreshInterval: 60, // minutes + RefreshRateLimit: 60, // seconds + RefreshTimeout: 10, // seconds + RefreshUnknownKID: true, + }) + }) + if JWKS == nil { + return + } + + jwtToken, err := jwt.Parse(strings.TrimPrefix(authHeader, "Bearer "), JWKS.Keyfunc) + if err != nil { + opt.Logger.Info().Err(err).Msg("Failed to parse/verify the access token.") + return + } + opt.Logger.Debug().Interface("access token", &jwtToken).Msg("parsed access token") + + if claims, ok := jwtToken.Claims.(jwt.MapClaims); ok && jwtToken.Valid { + ctx = oidc.NewContext(ctx, claims) + } + + default: + // do nothing + next.ServeHTTP(w, r.WithContext(ctx)) + return + } + + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +type jwksJSON struct { + JWKSURL string `json:"jwks_uri"` +} + +func getKeyfunc(log log.Logger, issuer string, client *http.Client, JwksOptions config.JWKS) *keyfunc.JWKS { + wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration" + + resp, err := client.Get(wellKnown) + if err != nil { + log.Error().Err(err).Msg("Failed to set request for .well-known/openid-configuration") + return nil + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Error().Err(err).Msg("unable to read discovery response body") + return nil + } + + if resp.StatusCode != http.StatusOK { + log.Error().Str("status", resp.Status).Str("body", string(body)).Msg("error requesting openid-configuration") + return nil + } + + var j jwksJSON + err = json.Unmarshal(body, &j) + if err != nil { + log.Error().Err(err).Msg("failed to decode provider openid-configuration") + return nil + } + log.Debug().Str("jwks", j.JWKSURL).Msg("discovered jwks endpoint") + options := keyfunc.Options{ + Client: client, + RefreshErrorHandler: func(err error) { + log.Error().Err(err).Msg("There was an error with the jwt.Keyfunc") + }, + RefreshInterval: time.Minute * time.Duration(JwksOptions.RefreshInterval), + RefreshRateLimit: time.Second * time.Duration(JwksOptions.RefreshRateLimit), + RefreshTimeout: time.Second * time.Duration(JwksOptions.RefreshTimeout), + RefreshUnknownKID: JwksOptions.RefreshUnknownKID, + } + JWKS, err := keyfunc.Get(j.JWKSURL, options) + if err != nil { + JWKS = nil + log.Error().Err(err).Msg("Failed to create JWKS from resource at the given URL.") + return nil + } + return JWKS +} diff --git a/ocis-pkg/middleware/options.go b/ocis-pkg/middleware/options.go new file mode 100644 index 00000000000..7feaa881718 --- /dev/null +++ b/ocis-pkg/middleware/options.go @@ -0,0 +1,31 @@ +package middleware + +import ( + gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + "github.com/owncloud/ocis/v2/ocis-pkg/log" +) + +// Option defines a single option function. +type Option func(o *Options) + +// Options defines the available options for this package. +type Options struct { + // Logger to use for logging, must be set + Logger log.Logger + // GatewayAPIClient is a reva gateway client + GatewayAPIClient gatewayv1beta1.GatewayAPIClient +} + +// WithLogger provides a function to set the logger option. +func WithLogger(val log.Logger) Option { + return func(o *Options) { + o.Logger = val + } +} + +// WithGatewayAPIClient provides a function to set the reva gateway client option. +func WithGatewayAPIClient(val gatewayv1beta1.GatewayAPIClient) Option { + return func(o *Options) { + o.GatewayAPIClient = val + } +} diff --git a/services/webfinger/README.md b/services/webfinger/README.md index dd84b0a3026..2d8587923fe 100644 --- a/services/webfinger/README.md +++ b/services/webfinger/README.md @@ -33,6 +33,8 @@ He will get a response like } ``` +As a special form of lookup clients can use the relation `acct:me@{host}` to look up the oCIS specific user id, username, email and displayname. + In this case there are two ocis instances and the client has to ask the user which instance he wants to use. ## TODO @@ -46,6 +48,11 @@ Currently, clients need to make subsequent calls to: - /ocs/v1.php/cloud/users/einstein to fetch the quota which could come from graph and actually is now tied to the spaces, not tu users - /graph/v1.0/me?%24expand=memberOf to fetch the user id and the groups the user is a member of +We need a way to pass oidc claims from the proxy, which does the authentication to the webfinger service, preferably by minting them into the internal reva token. +- Currently, we use machine auth so we can autoprovision an account if it does not exist. We should use revas oidc auth and, when autoprovisioning is enabled, retry the authentication after provisioning the account. This would allow us to use a `roles` claim to decide which roles to use and eg. a `school` claim to determine a specific instance. We may use https://github.com/PerimeterX/marshmallow to parse the RegisteredClaims and get the custom claims as a separate map. + +For now, webfinger can only match users based on a regex and produce a list of instances based on that. + Here are some Ideas which need to be discussed with all client teams in the future: ### status properties @@ -182,3 +189,20 @@ We could also embed subject metadata, however since apps like ocis web also need } ``` +# status php + +``` +{ + "subject": "https://drive.ocis.test", + "properties": { + "http://webfinger.owncloud/prop/maintenance": "false", + "http://webfinger.owncloud/prop/version": "10.11.0.6" + } + "links": [ + { + "rel": "http://openid.net/specs/connect/1.0/issuer" + "href": "https://idp.ocis.test", + }, + ] +} +``` \ No newline at end of file diff --git a/services/webfinger/pkg/command/server.go b/services/webfinger/pkg/command/server.go index 9bbe1dd67f2..4afaf14e599 100644 --- a/services/webfinger/pkg/command/server.go +++ b/services/webfinger/pkg/command/server.go @@ -3,6 +3,7 @@ package command import ( "context" "fmt" + "strings" "github.com/oklog/run" "github.com/owncloud/ocis/v2/ocis-pkg/config/configlog" @@ -11,6 +12,7 @@ import ( "github.com/owncloud/ocis/v2/services/webfinger/pkg/config/parser" "github.com/owncloud/ocis/v2/services/webfinger/pkg/logging" "github.com/owncloud/ocis/v2/services/webfinger/pkg/metrics" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/relations" "github.com/owncloud/ocis/v2/services/webfinger/pkg/server/debug" "github.com/owncloud/ocis/v2/services/webfinger/pkg/server/http" "github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0" @@ -53,11 +55,7 @@ func Server(cfg *config.Config) *cli.Command { svc, err := service.New( service.Logger(logger), service.Config(cfg), - service.WithInstanceSelector(getInstanceSelector(cfg.InstanceSelector)), - service.WithInstanceLookup(getInstanceLookup(cfg.InstanceLookup)), - // TODO pass in InstanceSelector - // TODO pass in InsanceLookup - + service.WithLookupChain(getLookupChain(cfg)), ) if err != nil { logger.Error().Err(err).Msg("handler init") @@ -119,16 +117,26 @@ func Server(cfg *config.Config) *cli.Command { } } -func getInstanceSelector(selector string) service.InstanceSelector { - switch selector { - default: - return service.DefaultInstanceSelector{} +func getLookupChain(cfg *config.Config) service.Webfinger { + lookups := strings.Split(cfg.LookupChain, ",") + if len(lookups) == 0 { + return nil } -} - -func getInstanceLookup(lookup string) service.InstanceLookup { - switch lookup { - default: - return service.DefaultInstanceLookup{} + var webfinger service.Webfinger + for i := len(lookups) - 1; i >= 0; i-- { + switch lookups[i] { + case "openid-discovery": + webfinger = relations.OpenIDDiscovery(cfg.IDP, webfinger) + case "owncloud-status": + case "owncloud-account": + //url, _ := url.Parse(cfg.OcisURL) + // TODO error / ignore + //webfinger = relations.OwnCloudAccount(*url, webfinger) + case "owncloud-instance": + webfinger = relations.OwnCloudInstance(cfg.Instances, webfinger) + default: + // TODO error / ignore + } } + return webfinger } diff --git a/services/webfinger/pkg/config/config.go b/services/webfinger/pkg/config/config.go index 16affe60974..3d7af2520c9 100644 --- a/services/webfinger/pkg/config/config.go +++ b/services/webfinger/pkg/config/config.go @@ -16,13 +16,27 @@ type Config struct { Log *Log `yaml:"log"` Debug Debug `yaml:"debug"` - HTTP HTTP `yaml:"http"` + HTTP HTTP `yaml:"http"` + Reva *shared.Reva `yaml:"reva"` - InstanceSelector string `yaml:"instance_selector" env:"WEBFINGER_INSTANCE_SELECTOR" desc:"How to select which instance to use for an account. Can be 'default', 'regex' or 'claims'?"` - InstanceLookup string `yaml:"instance_lookup" env:"WEBFINGER_INSTANCE_LOOKUP" desc:"How to look up to instance href and topic. Can be 'default', 'template', 'static' or 'ldap'?"` + Instances []Instance `yaml:"instances"` + InstanceSelector string `yaml:"instance_selector" env:"WEBFINGER_INSTANCE_SELECTOR" desc:"How to select which instance to use for an account. Can be 'default', 'regex' or 'claims'?"` + InstanceLookup string `yaml:"instance_lookup" env:"WEBFINGER_INSTANCE_LOOKUP" desc:"How to look up to instance href and topic. Can be 'default', 'template', 'static' or 'ldap'?"` + InstanceMatches string `yaml:"instance_matches" env:"WEBFINGER_INSTANCE_MATCHES" desc:"TODO"` + LookupChain string `yaml:"lookup_chain" env:"WEBFINGER_LOOKUP_CHAIN" desc:"A chain of lookup steps for webfinger."` + IDP string `yaml:"idp" env:"OCIS_URL;WEBFINGER_OIDC_ISSUER" desc:"The identity provider href for the openid-discovery relation."` + OcisURL string `yaml:"idp" env:"OCIS_URL;WEBFINGER_OCIS_URL" desc:"The oCIS instance URL for the owncloud-account relation. The host part will be used for the 'acct' URI."` Rules string `yaml:"webdav_namespace" env:"WEBFINGER_" desc:"Jail requests to /dav/webdav into this CS3 namespace. Supports template layouting with CS3 User properties."` // TODO wie proxy? Context context.Context `yaml:"-"` } + +// Instance to use with a matching rule and titles +type Instance struct { + Claim string `yaml:"claim"` + Regex string `yaml:"rule"` + Href string `yaml:"href"` + Titles map[string]string `yaml:"title"` +} diff --git a/services/webfinger/pkg/config/defaults/defaultconfig.go b/services/webfinger/pkg/config/defaults/defaultconfig.go index 5d53078f170..ab1ca2b7904 100644 --- a/services/webfinger/pkg/config/defaults/defaultconfig.go +++ b/services/webfinger/pkg/config/defaults/defaultconfig.go @@ -3,6 +3,7 @@ package defaults import ( "strings" + "github.com/owncloud/ocis/v2/ocis-pkg/shared" "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" ) @@ -31,9 +32,40 @@ func DefaultConfig() *config.Config { AllowedOrigins: []string{"*"}, }, }, + Reva: shared.DefaultRevaConfig(), Service: config.Service{ Name: "webfinger", }, + LookupChain: "openid-discovery,owncloud-status,owncloud-account,owncloud-instance", + Instances: []config.Instance{ + { + Claim: "mail", + Regex: "einstein@example.com", + Href: "{{OCIS_URL}}", + Titles: map[string]string{ + "en": "oCIS Instance for Einstein", + "de": "oCIS Instanz für Einstein", + }, + }, + { + Claim: "mail", + Regex: ".*@example.com", + Href: "{{OCIS_URL}}", + Titles: map[string]string{ + "en": "oCIS Instance for example.org", + "de": "oCIS Instanz für example.org", + }, + }, + { + Claim: "id", + Regex: ".*", + Href: "{{OCIS_URL}}", + Titles: map[string]string{ + "en": "oCIS Instance", + "de": "oCIS Instanz", + }, + }, + }, } } diff --git a/services/webfinger/pkg/relations/noop.go b/services/webfinger/pkg/relations/noop.go new file mode 100644 index 00000000000..acc33f064cc --- /dev/null +++ b/services/webfinger/pkg/relations/noop.go @@ -0,0 +1,15 @@ +package relations + +import ( + "context" + + "github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger" +) + +type Noop struct{} + +func (l Noop) Lookup(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) { +} +func (l Noop) Next(next service.Webfinger) { +} diff --git a/services/webfinger/pkg/relations/openid-discovery.go b/services/webfinger/pkg/relations/openid-discovery.go new file mode 100644 index 00000000000..732a6bb1f1a --- /dev/null +++ b/services/webfinger/pkg/relations/openid-discovery.go @@ -0,0 +1,43 @@ +package relations + +import ( + "context" + + "github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger" +) + +const ( + OpenIDConnectRel = "http://openid.net/specs/connect/1.0/issuer" +) + +type openIDDiscovery struct { + Href string + next service.Webfinger +} + +func OpenIDDiscovery(href string, next service.Webfinger) service.Webfinger { + if next == nil { + next = Noop{} + } + return &openIDDiscovery{ + Href: href, + next: next, + } +} + +func (l *openIDDiscovery) Lookup(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) { + if jrd == nil { + jrd = &webfinger.JSONResourceDescriptor{} + } + // TODO check if this relation was requested + jrd.Links = append(jrd.Links, webfinger.Link{ + Rel: OpenIDConnectRel, + Href: l.Href, + // Titles: , // TODO use , separated env var with : separated language -> title pairs + }) + l.next.Lookup(ctx, jrd) +} +func (l *openIDDiscovery) Next(next service.Webfinger) { + l.next = next +} diff --git a/services/webfinger/pkg/relations/owncloud-account.go b/services/webfinger/pkg/relations/owncloud-account.go new file mode 100644 index 00000000000..c04a74a2eaa --- /dev/null +++ b/services/webfinger/pkg/relations/owncloud-account.go @@ -0,0 +1,57 @@ +package relations + +import ( + "context" + "net/url" + "strings" + + revactx "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger" +) + +const ( + LibreGraphIDProp = "http://libregraph.org/prop/user/id" + LibreGraphSamAccountNameProp = "http://libregraph.org/prop/user/onPremisesSamAccountName" + LibreGraphMailProp = "http://libregraph.org/prop/user/mail" + LibreGraphDisplayNameProp = "http://libregraph.org/prop/user/displayName" +) + +type ownCloudAccount struct { + subject url.URL + next service.Webfinger +} + +func OwnCloudAccount(url url.URL, next service.Webfinger) service.Webfinger { + if next == nil { + next = Noop{} + } + + return &ownCloudAccount{ + subject: url, + next: next, + } +} + +func (l *ownCloudAccount) Lookup(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) { + if jrd == nil { + jrd = &webfinger.JSONResourceDescriptor{} + } + if strings.HasPrefix("acct:me", jrd.Subject) { + // TODO check if this relation was requested + if u, ok := revactx.ContextGetUser(ctx); ok { + // return correct account based on id + jrd.Subject = "acct:" + u.GetId().GetOpaqueId() + "@" + l.subject.Host + l.subject.Path + jrd.Properties[LibreGraphIDProp] = u.GetId().GetOpaqueId() + jrd.Properties[LibreGraphSamAccountNameProp] = u.GetUsername() + jrd.Properties[LibreGraphMailProp] = u.GetMail() + jrd.Properties[LibreGraphDisplayNameProp] = u.GetDisplayName() + } else { + // todo if we don't know the user return a 404, well, in this case a 401 + } + } + l.next.Lookup(ctx, jrd) +} +func (l *ownCloudAccount) Next(next service.Webfinger) { + l.next = next +} diff --git a/services/webfinger/pkg/relations/owncloud-instance.go b/services/webfinger/pkg/relations/owncloud-instance.go new file mode 100644 index 00000000000..91f01c473c0 --- /dev/null +++ b/services/webfinger/pkg/relations/owncloud-instance.go @@ -0,0 +1,67 @@ +package relations + +import ( + "context" + "regexp" + + "github.com/owncloud/ocis/v2/ocis-pkg/oidc" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger" +) + +const ( + OwnCloudInstanceRel = "http://webfinger.owncloud/rel/server-instance" +) + +type compiledInstance struct { + config.Instance + compiledRegex *regexp.Regexp +} + +type ownCloudInstance struct { + next service.Webfinger + instances []compiledInstance +} + +func OwnCloudInstance(instances []config.Instance, next service.Webfinger) service.Webfinger { + if next == nil { + next = Noop{} + } + compiledInstances := make([]compiledInstance, 0, len(instances)) + var err error + for _, instance := range instances { + compiled := compiledInstance{Instance: instance} + compiled.compiledRegex, err = regexp.Compile(instance.Regex) + if err != nil { + // TODO return error + } + } + + return &ownCloudInstance{ + instances: compiledInstances, + next: next, + } +} + +func (l *ownCloudInstance) Lookup(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) { + if jrd == nil { + jrd = &webfinger.JSONResourceDescriptor{} + } + if claims := oidc.FromContext(ctx); claims != nil { + for _, instance := range l.instances { + if value, ok := claims[instance.Claim].(string); ok && instance.compiledRegex.MatchString(value) { + jrd.Links = append(jrd.Links, webfinger.Link{ + Rel: OpenIDConnectRel, + Href: instance.Href, // allow a template? + Titles: instance.Titles, + }) + } + } + } + l.next.Lookup(ctx, jrd) +} + +func (l *ownCloudInstance) Next(next service.Webfinger) { + l.next = next +} diff --git a/services/webfinger/pkg/server/http/server.go b/services/webfinger/pkg/server/http/server.go index 090dd785ce1..5f810b60d02 100644 --- a/services/webfinger/pkg/server/http/server.go +++ b/services/webfinger/pkg/server/http/server.go @@ -2,7 +2,6 @@ package http import ( "context" - "errors" "net/http" "net/url" @@ -14,6 +13,7 @@ import ( ohttp "github.com/owncloud/ocis/v2/ocis-pkg/service/http" "github.com/owncloud/ocis/v2/ocis-pkg/version" serviceErrors "github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0" + "github.com/pkg/errors" "go-micro.dev/v4" ) @@ -60,6 +60,16 @@ func Server(opts ...Option) (ohttp.Service, error) { version.String, )) + // FIXME urgh we would have to do the auth ourself ... + // use auth bearer service ... + + // TODO use plain oidc claims: we don't want to have to call reva, which makes a call to ldap and also fetches groups ... + // + + mux.Use(middleware.OidcAuth( + middleware.WithLogger(options.Logger), + )) + // this logs http request related data mux.Use(middleware.Logger( options.Logger, diff --git a/services/webfinger/pkg/service/v0/option.go b/services/webfinger/pkg/service/v0/option.go index 3a93a7eb943..5ad5d15befa 100644 --- a/services/webfinger/pkg/service/v0/option.go +++ b/services/webfinger/pkg/service/v0/option.go @@ -10,10 +10,9 @@ type Option func(o *Options) // Options defines the available options for this package. type Options struct { - Logger log.Logger - Config *config.Config - InstanceIdSelector InstanceSelector - InstanceLookup InstanceLookup + Logger log.Logger + Config *config.Config + LookupChain Webfinger } // newOptions initializes the available default options. @@ -41,14 +40,8 @@ func Config(val *config.Config) Option { } } -func WithInstanceSelector(val InstanceSelector) Option { +func WithLookupChain(val Webfinger) Option { return func(o *Options) { - o.InstanceIdSelector = val - } -} - -func WithInstanceLookup(val InstanceLookup) Option { - return func(o *Options) { - o.InstanceLookup = val + o.LookupChain = val } } diff --git a/services/webfinger/pkg/service/v0/service.go b/services/webfinger/pkg/service/v0/service.go index 3e985dc3066..948a90bccc5 100644 --- a/services/webfinger/pkg/service/v0/service.go +++ b/services/webfinger/pkg/service/v0/service.go @@ -3,7 +3,6 @@ package service import ( "context" "net/url" - "strings" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" @@ -54,6 +53,11 @@ type Service interface { Webfinger(ctx context.Context, queryTarget *url.URL, rels []string) (webfinger.JSONResourceDescriptor, error) } +type Webfinger interface { + Lookup(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) + Next(next Webfinger) +} + type InstanceSelector interface { GetInstanceIds(ctx context.Context, account string) []string } @@ -92,28 +96,20 @@ func (l DefaultInstanceLookup) GetInstance(ctx context.Context, id string) Insta func New(opts ...Option) (Service, error) { options := newOptions(opts...) - if options.InstanceIdSelector == nil { - options.InstanceIdSelector = DefaultInstanceSelector{} - } - if options.InstanceLookup == nil { - options.InstanceLookup = DefaultInstanceLookup{} - } // TODO use fallback implementations of InstanceIdLookup and InstanceLookup? // The InstanceIdLookup may have to happen earlier? return svc{ - log: options.Logger, - config: options.Config, - instanceIdSelector: options.InstanceIdSelector, - instanceLookup: options.InstanceLookup, + log: options.Logger, + config: options.Config, + lookupChain: options.LookupChain, }, nil } type svc struct { - config *config.Config - log log.Logger - instanceIdSelector InstanceSelector - instanceLookup InstanceLookup + config *config.Config + log log.Logger + lookupChain Webfinger } // TODO implement different implementations: @@ -127,35 +123,49 @@ type svc struct { // Webfinger implements the service interface func (s svc) Webfinger(ctx context.Context, queryTarget *url.URL, rel []string) (webfinger.JSONResourceDescriptor, error) { - if queryTarget.Scheme != "acct" { - // wa can only handle acct lookup - return webfinger.JSONResourceDescriptor{}, ErrNotFound + jrd := webfinger.JSONResourceDescriptor{ + Subject: queryTarget.String(), } - instanceIds := s.instanceIdSelector.GetInstanceIds(ctx, strings.TrimPrefix(queryTarget.String(), "acct:")) - - href := ctx.Value("href").(string) - - links := make([]webfinger.Link, 0, len(instanceIds)) - // TODO, make listing oidc configuration optional - links = append(links, webfinger.Link{ - Rel: OpenIDConnectRel, - Href: href, - Titles: map[string]string{ - "en": "ownCloud Infinite Scale OpenID Connect Identity Provider", - }, - }) - for _, instanceId := range instanceIds { - instance := s.instanceLookup.GetInstance(ctx, instanceId) - links = append(links, webfinger.Link{ - Rel: OwnCloudInstanceRel, - Href: instance.Href, - Titles: instance.Titles, - }) + // TODO acct chain vs https: chain? + switch queryTarget.Scheme { + case "acct": + s.lookupChain.Lookup(ctx, &jrd) + case "http", "https": + s.lookupChain.Lookup(ctx, &jrd) + default: + return jrd, ErrNotFound } - return webfinger.JSONResourceDescriptor{ - Subject: queryTarget.String(), - Links: links, - }, nil + return jrd, nil + /* + instanceIds := s.instanceIdSelector.GetInstanceIds(ctx, strings.TrimPrefix(queryTarget.String(), "acct:")) + + href := ctx.Value("href").(string) + + links := make([]webfinger.Link, 0, len(instanceIds)) + // TODO, make listing oidc configuration optional + + links = append(links, webfinger.Link{ + Rel: OpenIDConnectRel, + Href: href, + Titles: map[string]string{ + "en": "ownCloud Infinite Scale OpenID Connect Identity Provider", + }, + }) + + for _, instanceId := range instanceIds { + instance := s.instanceLookup.GetInstance(ctx, instanceId) + links = append(links, webfinger.Link{ + Rel: OwnCloudInstanceRel, + Href: instance.Href, + Titles: instance.Titles, + }) + } + + return webfinger.JSONResourceDescriptor{ + Subject: queryTarget.String(), + Links: links, + }, nil + */ } From b11eeaa8cb95e839f861a5fd8fbcc0ae237c9a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Wed, 8 Feb 2023 16:06:11 +0000 Subject: [PATCH 07/23] add templating, drop chain, use map of relation providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- services/webfinger/pkg/command/server.go | 42 ++++---- services/webfinger/pkg/config/config.go | 18 ++-- .../pkg/config/defaults/defaultconfig.go | 54 +++++++--- services/webfinger/pkg/relations/noop.go | 15 --- .../pkg/relations/openid-discovery.go | 13 +-- .../pkg/relations/owncloud-account.go | 57 ---------- .../pkg/relations/owncloud-instance.go | 39 ++++--- services/webfinger/pkg/server/http/server.go | 16 --- services/webfinger/pkg/service/v0/option.go | 10 +- services/webfinger/pkg/service/v0/service.go | 101 ++++-------------- 10 files changed, 115 insertions(+), 250 deletions(-) delete mode 100644 services/webfinger/pkg/relations/noop.go delete mode 100644 services/webfinger/pkg/relations/owncloud-account.go diff --git a/services/webfinger/pkg/command/server.go b/services/webfinger/pkg/command/server.go index 4afaf14e599..12cfa846694 100644 --- a/services/webfinger/pkg/command/server.go +++ b/services/webfinger/pkg/command/server.go @@ -3,7 +3,6 @@ package command import ( "context" "fmt" - "strings" "github.com/oklog/run" "github.com/owncloud/ocis/v2/ocis-pkg/config/configlog" @@ -52,10 +51,16 @@ func Server(cfg *config.Config) *cli.Command { metrics.BuildInfo.WithLabelValues(version.GetString()).Set(1) { + relationProviders, err := getRelationProviders(cfg) + if err != nil { + logger.Error().Err(err).Msg("relation providier init") + return err + } + svc, err := service.New( service.Logger(logger), service.Config(cfg), - service.WithLookupChain(getLookupChain(cfg)), + service.WithRelationProviders(relationProviders), ) if err != nil { logger.Error().Err(err).Msg("handler init") @@ -117,26 +122,21 @@ func Server(cfg *config.Config) *cli.Command { } } -func getLookupChain(cfg *config.Config) service.Webfinger { - lookups := strings.Split(cfg.LookupChain, ",") - if len(lookups) == 0 { - return nil - } - var webfinger service.Webfinger - for i := len(lookups) - 1; i >= 0; i-- { - switch lookups[i] { - case "openid-discovery": - webfinger = relations.OpenIDDiscovery(cfg.IDP, webfinger) - case "owncloud-status": - case "owncloud-account": - //url, _ := url.Parse(cfg.OcisURL) - // TODO error / ignore - //webfinger = relations.OwnCloudAccount(*url, webfinger) - case "owncloud-instance": - webfinger = relations.OwnCloudInstance(cfg.Instances, webfinger) +func getRelationProviders(cfg *config.Config) (map[string]service.RelationProvider, error) { + rels := map[string]service.RelationProvider{} + for _, relationURI := range cfg.Relations { + switch relationURI { + case relations.OpenIDConnectRel: + rels[relationURI] = relations.OpenIDDiscovery(cfg.IDP) + case relations.OwnCloudInstanceRel: + var err error + rels[relationURI], err = relations.OwnCloudInstance(cfg.Instances, cfg.OcisURL) + if err != nil { + return nil, err + } default: - // TODO error / ignore + return nil, fmt.Errorf("unknown relation '%s'", relationURI) } } - return webfinger + return rels, nil } diff --git a/services/webfinger/pkg/config/config.go b/services/webfinger/pkg/config/config.go index 3d7af2520c9..0a28e0c4f3c 100644 --- a/services/webfinger/pkg/config/config.go +++ b/services/webfinger/pkg/config/config.go @@ -16,19 +16,12 @@ type Config struct { Log *Log `yaml:"log"` Debug Debug `yaml:"debug"` - HTTP HTTP `yaml:"http"` - Reva *shared.Reva `yaml:"reva"` + HTTP HTTP `yaml:"http"` - Instances []Instance `yaml:"instances"` - InstanceSelector string `yaml:"instance_selector" env:"WEBFINGER_INSTANCE_SELECTOR" desc:"How to select which instance to use for an account. Can be 'default', 'regex' or 'claims'?"` - InstanceLookup string `yaml:"instance_lookup" env:"WEBFINGER_INSTANCE_LOOKUP" desc:"How to look up to instance href and topic. Can be 'default', 'template', 'static' or 'ldap'?"` - InstanceMatches string `yaml:"instance_matches" env:"WEBFINGER_INSTANCE_MATCHES" desc:"TODO"` - LookupChain string `yaml:"lookup_chain" env:"WEBFINGER_LOOKUP_CHAIN" desc:"A chain of lookup steps for webfinger."` - IDP string `yaml:"idp" env:"OCIS_URL;WEBFINGER_OIDC_ISSUER" desc:"The identity provider href for the openid-discovery relation."` - OcisURL string `yaml:"idp" env:"OCIS_URL;WEBFINGER_OCIS_URL" desc:"The oCIS instance URL for the owncloud-account relation. The host part will be used for the 'acct' URI."` - - Rules string `yaml:"webdav_namespace" env:"WEBFINGER_" desc:"Jail requests to /dav/webdav into this CS3 namespace. Supports template layouting with CS3 User properties."` - // TODO wie proxy? + Instances []Instance `yaml:"instances"` + Relations []string `yaml:"relations" env:"WEBFINGER_RELATIONS" desc:"A comma-separated list of relation URIs or registered relation types to add to webfinger responses."` + IDP string `yaml:"idp" env:"OCIS_URL;WEBFINGER_OIDC_ISSUER" desc:"The identity provider href for the openid-discovery relation."` + OcisURL string `yaml:"idp" env:"OCIS_URL;WEBFINGER_OCIS_URL" desc:"The oCIS instance URL for the owncloud instance relations."` Context context.Context `yaml:"-"` } @@ -39,4 +32,5 @@ type Instance struct { Regex string `yaml:"rule"` Href string `yaml:"href"` Titles map[string]string `yaml:"title"` + Break bool `yaml:"break"` } diff --git a/services/webfinger/pkg/config/defaults/defaultconfig.go b/services/webfinger/pkg/config/defaults/defaultconfig.go index ab1ca2b7904..d160157d18c 100644 --- a/services/webfinger/pkg/config/defaults/defaultconfig.go +++ b/services/webfinger/pkg/config/defaults/defaultconfig.go @@ -3,8 +3,8 @@ package defaults import ( "strings" - "github.com/owncloud/ocis/v2/ocis-pkg/shared" "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/relations" ) func FullDefaultConfig() *config.Config { @@ -17,49 +17,71 @@ func FullDefaultConfig() *config.Config { func DefaultConfig() *config.Config { return &config.Config{ Debug: config.Debug{ - Addr: "127.0.0.1:19119", // FIXME - //Addr: "127.0.0.1:0", // :0 to pick any free local port + //Addr: "127.0.0.1:19119", // FIXME + Addr: "127.0.0.1:0", // :0 to pick any free local port Token: "", Pprof: false, Zpages: false, }, HTTP: config.HTTP{ - Addr: "127.0.0.1:19115", // FIXME - //Addr: "127.0.0.1:0", // :0 to pick any free local port + //Addr: "127.0.0.1:19115", // FIXME + Addr: "127.0.0.1:0", // :0 to pick any free local port Root: "/", Namespace: "com.owncloud.web", CORS: config.CORS{ AllowedOrigins: []string{"*"}, }, }, - Reva: shared.DefaultRevaConfig(), Service: config.Service{ Name: "webfinger", }, - LookupChain: "openid-discovery,owncloud-status,owncloud-account,owncloud-instance", + + Relations: []string{relations.OpenIDConnectRel, relations.OwnCloudInstanceRel}, Instances: []config.Instance{ { - Claim: "mail", - Regex: "einstein@example.com", - Href: "{{OCIS_URL}}", + Claim: "email", + Regex: "einstein@example\\.org", // only einstein + Href: "{{.OCIS_URL}}", Titles: map[string]string{ "en": "oCIS Instance for Einstein", "de": "oCIS Instanz für Einstein", }, + Break: true, + }, + { + Claim: "email", + Regex: "marie@example\\.org", // only marie + Href: "https://{{.preferred_username}}.cloud.ocis.test", + Titles: map[string]string{ + "en": "oCIS Instance for Marie", + "de": "oCIS Instanz für Marie", + }, + // also continue with next instance }, { - Claim: "mail", - Regex: ".*@example.com", - Href: "{{OCIS_URL}}", + Claim: "email", + Regex: ".+@example\\.org", // example.org, including marie but not for einstein + Href: "{{.OCIS_URL}}", // zb https://{{schoolid}}.cloud.ocis.de bei dem der schoolid claim dann genommen wird. templates? Titles: map[string]string{ "en": "oCIS Instance for example.org", "de": "oCIS Instanz für example.org", }, + Break: true, + }, + { + Claim: "email", + Regex: ".+@example\\.com", // example.com + Href: "{{.OCIS_URL}}", + Titles: map[string]string{ + "en": "oCIS Instance for example.com", + "de": "oCIS Instanz für example.com", + }, + Break: true, }, { - Claim: "id", - Regex: ".*", - Href: "{{OCIS_URL}}", + Claim: "email", + Regex: ".+", + Href: "{{.OCIS_URL}}", Titles: map[string]string{ "en": "oCIS Instance", "de": "oCIS Instanz", diff --git a/services/webfinger/pkg/relations/noop.go b/services/webfinger/pkg/relations/noop.go deleted file mode 100644 index acc33f064cc..00000000000 --- a/services/webfinger/pkg/relations/noop.go +++ /dev/null @@ -1,15 +0,0 @@ -package relations - -import ( - "context" - - "github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0" - "github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger" -) - -type Noop struct{} - -func (l Noop) Lookup(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) { -} -func (l Noop) Next(next service.Webfinger) { -} diff --git a/services/webfinger/pkg/relations/openid-discovery.go b/services/webfinger/pkg/relations/openid-discovery.go index 732a6bb1f1a..86fe898df1a 100644 --- a/services/webfinger/pkg/relations/openid-discovery.go +++ b/services/webfinger/pkg/relations/openid-discovery.go @@ -13,20 +13,15 @@ const ( type openIDDiscovery struct { Href string - next service.Webfinger } -func OpenIDDiscovery(href string, next service.Webfinger) service.Webfinger { - if next == nil { - next = Noop{} - } +func OpenIDDiscovery(href string) service.RelationProvider { return &openIDDiscovery{ Href: href, - next: next, } } -func (l *openIDDiscovery) Lookup(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) { +func (l *openIDDiscovery) Add(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) { if jrd == nil { jrd = &webfinger.JSONResourceDescriptor{} } @@ -36,8 +31,4 @@ func (l *openIDDiscovery) Lookup(ctx context.Context, jrd *webfinger.JSONResourc Href: l.Href, // Titles: , // TODO use , separated env var with : separated language -> title pairs }) - l.next.Lookup(ctx, jrd) -} -func (l *openIDDiscovery) Next(next service.Webfinger) { - l.next = next } diff --git a/services/webfinger/pkg/relations/owncloud-account.go b/services/webfinger/pkg/relations/owncloud-account.go deleted file mode 100644 index c04a74a2eaa..00000000000 --- a/services/webfinger/pkg/relations/owncloud-account.go +++ /dev/null @@ -1,57 +0,0 @@ -package relations - -import ( - "context" - "net/url" - "strings" - - revactx "github.com/cs3org/reva/v2/pkg/ctx" - "github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0" - "github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger" -) - -const ( - LibreGraphIDProp = "http://libregraph.org/prop/user/id" - LibreGraphSamAccountNameProp = "http://libregraph.org/prop/user/onPremisesSamAccountName" - LibreGraphMailProp = "http://libregraph.org/prop/user/mail" - LibreGraphDisplayNameProp = "http://libregraph.org/prop/user/displayName" -) - -type ownCloudAccount struct { - subject url.URL - next service.Webfinger -} - -func OwnCloudAccount(url url.URL, next service.Webfinger) service.Webfinger { - if next == nil { - next = Noop{} - } - - return &ownCloudAccount{ - subject: url, - next: next, - } -} - -func (l *ownCloudAccount) Lookup(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) { - if jrd == nil { - jrd = &webfinger.JSONResourceDescriptor{} - } - if strings.HasPrefix("acct:me", jrd.Subject) { - // TODO check if this relation was requested - if u, ok := revactx.ContextGetUser(ctx); ok { - // return correct account based on id - jrd.Subject = "acct:" + u.GetId().GetOpaqueId() + "@" + l.subject.Host + l.subject.Path - jrd.Properties[LibreGraphIDProp] = u.GetId().GetOpaqueId() - jrd.Properties[LibreGraphSamAccountNameProp] = u.GetUsername() - jrd.Properties[LibreGraphMailProp] = u.GetMail() - jrd.Properties[LibreGraphDisplayNameProp] = u.GetDisplayName() - } else { - // todo if we don't know the user return a 404, well, in this case a 401 - } - } - l.next.Lookup(ctx, jrd) -} -func (l *ownCloudAccount) Next(next service.Webfinger) { - l.next = next -} diff --git a/services/webfinger/pkg/relations/owncloud-instance.go b/services/webfinger/pkg/relations/owncloud-instance.go index 91f01c473c0..e2e2c6ebcd1 100644 --- a/services/webfinger/pkg/relations/owncloud-instance.go +++ b/services/webfinger/pkg/relations/owncloud-instance.go @@ -3,6 +3,8 @@ package relations import ( "context" "regexp" + "strings" + "text/template" "github.com/owncloud/ocis/v2/ocis-pkg/oidc" "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" @@ -17,51 +19,56 @@ const ( type compiledInstance struct { config.Instance compiledRegex *regexp.Regexp + hrefTemplate *template.Template } type ownCloudInstance struct { - next service.Webfinger instances []compiledInstance + ocisURL string } -func OwnCloudInstance(instances []config.Instance, next service.Webfinger) service.Webfinger { - if next == nil { - next = Noop{} - } +func OwnCloudInstance(instances []config.Instance, ocisURL string) (service.RelationProvider, error) { compiledInstances := make([]compiledInstance, 0, len(instances)) var err error for _, instance := range instances { compiled := compiledInstance{Instance: instance} compiled.compiledRegex, err = regexp.Compile(instance.Regex) if err != nil { - // TODO return error + return nil, err + } + compiled.hrefTemplate, err = template.New(instance.Claim + ":" + instance.Regex + ":" + instance.Href).Parse(instance.Href) + if err != nil { + return nil, err } + compiledInstances = append(compiledInstances, compiled) } return &ownCloudInstance{ instances: compiledInstances, - next: next, - } + ocisURL: ocisURL, + }, nil } -func (l *ownCloudInstance) Lookup(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) { +func (l *ownCloudInstance) Add(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) { if jrd == nil { jrd = &webfinger.JSONResourceDescriptor{} } if claims := oidc.FromContext(ctx); claims != nil { + // allow referencing OCIS_URL in the template + claims["OCIS_URL"] = l.ocisURL for _, instance := range l.instances { if value, ok := claims[instance.Claim].(string); ok && instance.compiledRegex.MatchString(value) { + var tmplWriter strings.Builder + instance.hrefTemplate.Execute(&tmplWriter, claims) jrd.Links = append(jrd.Links, webfinger.Link{ - Rel: OpenIDConnectRel, - Href: instance.Href, // allow a template? + Rel: OwnCloudInstanceRel, + Href: tmplWriter.String(), Titles: instance.Titles, }) + if instance.Break { + break + } } } } - l.next.Lookup(ctx, jrd) -} - -func (l *ownCloudInstance) Next(next service.Webfinger) { - l.next = next } diff --git a/services/webfinger/pkg/server/http/server.go b/services/webfinger/pkg/server/http/server.go index 5f810b60d02..df3f6c488c4 100644 --- a/services/webfinger/pkg/server/http/server.go +++ b/services/webfinger/pkg/server/http/server.go @@ -1,7 +1,6 @@ package http import ( - "context" "net/http" "net/url" @@ -80,9 +79,6 @@ func Server(opts ...Option) (ohttp.Service, error) { r.Get("/.well-known/webfinger", func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - // for now, put the url in the context so it can be used to fake a list - ctx = context.WithValue(ctx, "href", getHref(r)) - // A WebFinger URI MUST contain a query component (see Section 3.4 of // RFC 3986). The query component MUST contain a "resource" parameter // and MAY contain one or more "rel" parameters. @@ -135,15 +131,3 @@ func Server(opts ...Option) (ohttp.Service, error) { svc.Init() return svc, nil } - -func getHref(r *http.Request) string { - proto := r.Header.Get("x-forwarded-proto") - host := r.Header.Get("x-forwarded-host") - port := r.Header.Get("x-forwarded-port") - - if (proto == "http" && port != "80") || (proto == "https" && port != "443") { - host = host + ":" + port - } - - return proto + "://" + host -} diff --git a/services/webfinger/pkg/service/v0/option.go b/services/webfinger/pkg/service/v0/option.go index 5ad5d15befa..477b793bcb5 100644 --- a/services/webfinger/pkg/service/v0/option.go +++ b/services/webfinger/pkg/service/v0/option.go @@ -10,9 +10,9 @@ type Option func(o *Options) // Options defines the available options for this package. type Options struct { - Logger log.Logger - Config *config.Config - LookupChain Webfinger + Logger log.Logger + Config *config.Config + RelationProviders map[string]RelationProvider } // newOptions initializes the available default options. @@ -40,8 +40,8 @@ func Config(val *config.Config) Option { } } -func WithLookupChain(val Webfinger) Option { +func WithRelationProviders(val map[string]RelationProvider) Option { return func(o *Options) { - o.LookupChain = val + o.RelationProviders = val } } diff --git a/services/webfinger/pkg/service/v0/service.go b/services/webfinger/pkg/service/v0/service.go index 948a90bccc5..238fbbf64e8 100644 --- a/services/webfinger/pkg/service/v0/service.go +++ b/services/webfinger/pkg/service/v0/service.go @@ -53,43 +53,8 @@ type Service interface { Webfinger(ctx context.Context, queryTarget *url.URL, rels []string) (webfinger.JSONResourceDescriptor, error) } -type Webfinger interface { - Lookup(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) - Next(next Webfinger) -} - -type InstanceSelector interface { - GetInstanceIds(ctx context.Context, account string) []string -} - -type InstanceLookup interface { - GetInstance(ctx context.Context, id string) Instance - // get multiple instances at once? -} - -type Instance struct { - Href string - Titles map[string]string -} - -type DefaultInstanceSelector struct{} - -func (s DefaultInstanceSelector) GetInstanceIds(ctx context.Context, account string) []string { - return []string{"default"} -} - -type DefaultInstanceLookup struct{} - -func (l DefaultInstanceLookup) GetInstance(ctx context.Context, id string) Instance { - if id == "default" { - return Instance{ - Href: ctx.Value("href").(string), - Titles: map[string]string{ - "en": "ownCloud Infinite Scale", - }, - } - } - return Instance{} +type RelationProvider interface { + Add(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) } // New returns a new instance of Service @@ -100,16 +65,16 @@ func New(opts ...Option) (Service, error) { // The InstanceIdLookup may have to happen earlier? return svc{ - log: options.Logger, - config: options.Config, - lookupChain: options.LookupChain, + log: options.Logger, + config: options.Config, + relationProviders: options.RelationProviders, }, nil } type svc struct { - config *config.Config - log log.Logger - lookupChain Webfinger + config *config.Config + log log.Logger + relationProviders map[string]RelationProvider } // TODO implement different implementations: @@ -127,45 +92,19 @@ func (s svc) Webfinger(ctx context.Context, queryTarget *url.URL, rel []string) Subject: queryTarget.String(), } - // TODO acct chain vs https: chain? - switch queryTarget.Scheme { - case "acct": - s.lookupChain.Lookup(ctx, &jrd) - case "http", "https": - s.lookupChain.Lookup(ctx, &jrd) - default: - return jrd, ErrNotFound + if len(rel) == 0 { + // add all configured relation providers + for _, relation := range s.relationProviders { + relation.Add(ctx, &jrd) + } + } else { + // only add requested relations + for _, r := range rel { + if relation, ok := s.relationProviders[r]; ok { + relation.Add(ctx, &jrd) + } + } } return jrd, nil - /* - instanceIds := s.instanceIdSelector.GetInstanceIds(ctx, strings.TrimPrefix(queryTarget.String(), "acct:")) - - href := ctx.Value("href").(string) - - links := make([]webfinger.Link, 0, len(instanceIds)) - // TODO, make listing oidc configuration optional - - links = append(links, webfinger.Link{ - Rel: OpenIDConnectRel, - Href: href, - Titles: map[string]string{ - "en": "ownCloud Infinite Scale OpenID Connect Identity Provider", - }, - }) - - for _, instanceId := range instanceIds { - instance := s.instanceLookup.GetInstance(ctx, instanceId) - links = append(links, webfinger.Link{ - Rel: OwnCloudInstanceRel, - Href: instance.Href, - Titles: instance.Titles, - }) - } - - return webfinger.JSONResourceDescriptor{ - Subject: queryTarget.String(), - Links: links, - }, nil - */ } From 66704194e87495d41e2fbb53489252e5214def6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Wed, 8 Feb 2023 16:23:34 +0000 Subject: [PATCH 08/23] fix ocis url yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- services/webfinger/pkg/config/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/webfinger/pkg/config/config.go b/services/webfinger/pkg/config/config.go index 0a28e0c4f3c..51c94e1f9ed 100644 --- a/services/webfinger/pkg/config/config.go +++ b/services/webfinger/pkg/config/config.go @@ -20,8 +20,8 @@ type Config struct { Instances []Instance `yaml:"instances"` Relations []string `yaml:"relations" env:"WEBFINGER_RELATIONS" desc:"A comma-separated list of relation URIs or registered relation types to add to webfinger responses."` - IDP string `yaml:"idp" env:"OCIS_URL;WEBFINGER_OIDC_ISSUER" desc:"The identity provider href for the openid-discovery relation."` - OcisURL string `yaml:"idp" env:"OCIS_URL;WEBFINGER_OCIS_URL" desc:"The oCIS instance URL for the owncloud instance relations."` + IDP string `yaml:"idp" env:"OCIS_URL;OCIS_OIDC_ISSUER;WEBFINGER_OIDC_ISSUER" desc:"The identity provider href for the openid-discovery relation."` + OcisURL string `yaml:"ocis_url" env:"OCIS_URL;WEBFINGER_OCIS_URL" desc:"URL, where oCIS is reachable for users."` Context context.Context `yaml:"-"` } From ee97f6f16a9fb477e595b7154f4366ca64e1f85f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Wed, 8 Feb 2023 17:59:43 +0100 Subject: [PATCH 09/23] fix typos Co-authored-by: Dominik Schmidt --- services/webfinger/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/webfinger/README.md b/services/webfinger/README.md index 2d8587923fe..16117f54c99 100644 --- a/services/webfinger/README.md +++ b/services/webfinger/README.md @@ -6,7 +6,7 @@ It is based on https://github.com/owncloud/lookup-webfinger-sciebo but also retu A client can discover various endpoints by making a `GET` request to `https://drive.ocis.test/.well-known/webfinger?resource=acct%3Aeinstein%40cloud.ocis.test`. -He will get a response like +It will get a response like ``` { "subject": "acct:einstein@drive.ocis.test", @@ -45,7 +45,7 @@ Currently, clients need to make subsequent calls to: - /.well-known/openid-configuration, auth2 token and oidc userinfo endpoints to authenticate the user - /ocs/v1.php/cloud/user to get the username, eg. einstein ... again? it contains the oc10 user id (marie, not the uuid) - /ocs/v1.php/cloud/capabilities to fetch instance capabilites -- /ocs/v1.php/cloud/users/einstein to fetch the quota which could come from graph and actually is now tied to the spaces, not tu users +- /ocs/v1.php/cloud/users/einstein to fetch the quota which could come from graph and actually is now tied to the spaces, not to users - /graph/v1.0/me?%24expand=memberOf to fetch the user id and the groups the user is a member of We need a way to pass oidc claims from the proxy, which does the authentication to the webfinger service, preferably by minting them into the internal reva token. @@ -156,7 +156,7 @@ In theory the graph endpoint would allow discovering drives on any domain. But t ### Subject properties -We could also embed subject metadata, however since apps like ocis web also need the groups a user is member of a dadicated call to the libregraph api is probably better. In any case, we could return properties for the subject: +We could also embed subject metadata, however since apps like ocis web also need the groups a user is member of a dedicated call to the libregraph api is probably better. In any case, we could return properties for the subject: ``` { "subject": "acct:einstein@drive.ocis.test", From 0b91c93b49e8fcb0c429fed314e5638cac081d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Thu, 9 Feb 2023 21:02:06 +0000 Subject: [PATCH 10/23] switch to userinfo claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- ocis-pkg/middleware/oidc.go | 117 ++++------ ocis-pkg/middleware/options.go | 9 + services/webfinger/README.md | 219 ++++++------------ services/webfinger/pkg/config/config.go | 4 +- .../pkg/config/defaults/defaultconfig.go | 46 +--- services/webfinger/pkg/server/http/server.go | 7 +- 6 files changed, 124 insertions(+), 278 deletions(-) diff --git a/ocis-pkg/middleware/oidc.go b/ocis-pkg/middleware/oidc.go index d29b1793477..c6bcefbaea1 100644 --- a/ocis-pkg/middleware/oidc.go +++ b/ocis-pkg/middleware/oidc.go @@ -1,18 +1,14 @@ package middleware import ( - "encoding/json" - "io" + "context" "net/http" "strings" "sync" - "time" - "github.com/MicahParks/keyfunc" - "github.com/golang-jwt/jwt/v4" - "github.com/owncloud/ocis/v2/ocis-pkg/log" + gOidc "github.com/coreos/go-oidc/v3/oidc" "github.com/owncloud/ocis/v2/ocis-pkg/oidc" - "github.com/owncloud/ocis/v2/services/proxy/pkg/config" + "golang.org/x/oauth2" ) // newOidcOptions initializes the available default options. @@ -26,6 +22,11 @@ func newOidcOptions(opts ...Option) Options { return opt } +// OIDCProvider used to mock the oidc provider during tests +type OIDCProvider interface { + UserInfo(ctx context.Context, ts oauth2.TokenSource) (*gOidc.UserInfo, error) +} + // OidcAuth provides a middleware to authenticate a bearer auth with an OpenID Connect identity provider // It will put all claims provided by the userinfo endpoint in the context func OidcAuth(opts ...Option) func(http.Handler) http.Handler { @@ -33,9 +34,17 @@ func OidcAuth(opts ...Option) func(http.Handler) http.Handler { // TODO use a micro store cache option - var JWKS *keyfunc.JWKS - getKeyfuncOnce := sync.Once{} - issuer := "https://cloud.ocis.test" + providerFunc := func() (OIDCProvider, error) { + // Initialize a provider by specifying the issuer URL. + // it will fetch the keys from the issuer using the .well-known + // endpoint + return gOidc.NewProvider( + context.WithValue(context.Background(), oauth2.HTTPClient, http.Client{}), + opt.OidcIssuer, + ) + } + var provider OIDCProvider + getProviderOnce := sync.Once{} return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -43,29 +52,34 @@ func OidcAuth(opts ...Option) func(http.Handler) http.Handler { authHeader := r.Header.Get("Authorization") switch { case strings.HasPrefix(authHeader, "Bearer "): - getKeyfuncOnce.Do(func() { - JWKS = getKeyfunc(opt.Logger, issuer, &http.Client{}, config.JWKS{ - RefreshInterval: 60, // minutes - RefreshRateLimit: 60, // seconds - RefreshTimeout: 10, // seconds - RefreshUnknownKID: true, - }) + getProviderOnce.Do(func() { + var err error + provider, err = providerFunc() + if err != nil { + return + } }) - if JWKS == nil { - return + + oauth2Token := &oauth2.Token{ + AccessToken: strings.TrimPrefix(authHeader, "Bearer "), } - jwtToken, err := jwt.Parse(strings.TrimPrefix(authHeader, "Bearer "), JWKS.Keyfunc) + userInfo, err := provider.UserInfo( + context.WithValue(ctx, oauth2.HTTPClient, http.Client{}), + oauth2.StaticTokenSource(oauth2Token), + ) if err != nil { - opt.Logger.Info().Err(err).Msg("Failed to parse/verify the access token.") - return + // what if unauthorized + break } - opt.Logger.Debug().Interface("access token", &jwtToken).Msg("parsed access token") - - if claims, ok := jwtToken.Claims.(jwt.MapClaims); ok && jwtToken.Valid { - ctx = oidc.NewContext(ctx, claims) + claims := map[string]interface{}{} + err = userInfo.Claims(&claims) + if err != nil { + break } + ctx = oidc.NewContext(ctx, claims) + default: // do nothing next.ServeHTTP(w, r.WithContext(ctx)) @@ -76,54 +90,3 @@ func OidcAuth(opts ...Option) func(http.Handler) http.Handler { }) } } - -type jwksJSON struct { - JWKSURL string `json:"jwks_uri"` -} - -func getKeyfunc(log log.Logger, issuer string, client *http.Client, JwksOptions config.JWKS) *keyfunc.JWKS { - wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration" - - resp, err := client.Get(wellKnown) - if err != nil { - log.Error().Err(err).Msg("Failed to set request for .well-known/openid-configuration") - return nil - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - log.Error().Err(err).Msg("unable to read discovery response body") - return nil - } - - if resp.StatusCode != http.StatusOK { - log.Error().Str("status", resp.Status).Str("body", string(body)).Msg("error requesting openid-configuration") - return nil - } - - var j jwksJSON - err = json.Unmarshal(body, &j) - if err != nil { - log.Error().Err(err).Msg("failed to decode provider openid-configuration") - return nil - } - log.Debug().Str("jwks", j.JWKSURL).Msg("discovered jwks endpoint") - options := keyfunc.Options{ - Client: client, - RefreshErrorHandler: func(err error) { - log.Error().Err(err).Msg("There was an error with the jwt.Keyfunc") - }, - RefreshInterval: time.Minute * time.Duration(JwksOptions.RefreshInterval), - RefreshRateLimit: time.Second * time.Duration(JwksOptions.RefreshRateLimit), - RefreshTimeout: time.Second * time.Duration(JwksOptions.RefreshTimeout), - RefreshUnknownKID: JwksOptions.RefreshUnknownKID, - } - JWKS, err := keyfunc.Get(j.JWKSURL, options) - if err != nil { - JWKS = nil - log.Error().Err(err).Msg("Failed to create JWKS from resource at the given URL.") - return nil - } - return JWKS -} diff --git a/ocis-pkg/middleware/options.go b/ocis-pkg/middleware/options.go index 7feaa881718..d1a8eff6bcf 100644 --- a/ocis-pkg/middleware/options.go +++ b/ocis-pkg/middleware/options.go @@ -12,10 +12,19 @@ type Option func(o *Options) type Options struct { // Logger to use for logging, must be set Logger log.Logger + // The OpenID Connect Issuer URL + OidcIssuer string // GatewayAPIClient is a reva gateway client GatewayAPIClient gatewayv1beta1.GatewayAPIClient } +// WithLogger provides a function to set the openid connect issuer option. +func WithOidcIssuer(val string) Option { + return func(o *Options) { + o.OidcIssuer = val + } +} + // WithLogger provides a function to set the logger option. func WithLogger(val log.Logger) Option { return func(o *Options) { diff --git a/services/webfinger/README.md b/services/webfinger/README.md index 16117f54c99..896042e9983 100644 --- a/services/webfinger/README.md +++ b/services/webfinger/README.md @@ -4,9 +4,9 @@ The webfinger service provides an RFC7033 WebFinger lookup of ownCloud instances It is based on https://github.com/owncloud/lookup-webfinger-sciebo but also returns localized `titles` in addition to the `href` property. -A client can discover various endpoints by making a `GET` request to `https://drive.ocis.test/.well-known/webfinger?resource=acct%3Aeinstein%40cloud.ocis.test`. +## OpenID Connect Discovery -It will get a response like +Clients can make an unauthenticated `GET https://drive.ocis.test/.well-known/webfinger?resource=https%3A%2F%2Fcloud.ocis.test` request to discover the OpenID Connect Issuer in the `http://openid.net/specs/connect/1.0/issuer` relation: ``` { "subject": "acct:einstein@drive.ocis.test", @@ -14,195 +14,118 @@ It will get a response like { "rel": "http://openid.net/specs/connect/1.0/issuer" "href": "https://sso.example.org/cas/oidc/", - }, - { - "rel": "http://webfinger.owncloud/rel/server-instance", - "href": "https://abc.drive.example.org" - "titles": { - "en": "Readable Instance Name" - } - }, - { - "rel": "http://webfinger.owncloud/rel/server-instance", - "href": "https://xyz.drive.example.org" - "titles": { - "en": "Readable Other Instance Name" - } - }, + } ] } ``` -As a special form of lookup clients can use the relation `acct:me@{host}` to look up the oCIS specific user id, username, email and displayname. - -In this case there are two ocis instances and the client has to ask the user which instance he wants to use. - -## TODO -Currently, clients need to make subsequent calls to: -- /status.php to check if the instance is in maintenance mode or if the version is supported -- /config.json to get the available apps for ocis web to determine which routes require authentication -- /themes/owncloud/theme.json for theming info -- /.well-known/openid-configuration, auth2 token and oidc userinfo endpoints to authenticate the user -- /ocs/v1.php/cloud/user to get the username, eg. einstein ... again? it contains the oc10 user id (marie, not the uuid) -- /ocs/v1.php/cloud/capabilities to fetch instance capabilites -- /ocs/v1.php/cloud/users/einstein to fetch the quota which could come from graph and actually is now tied to the spaces, not to users -- /graph/v1.0/me?%24expand=memberOf to fetch the user id and the groups the user is a member of - -We need a way to pass oidc claims from the proxy, which does the authentication to the webfinger service, preferably by minting them into the internal reva token. -- Currently, we use machine auth so we can autoprovision an account if it does not exist. We should use revas oidc auth and, when autoprovisioning is enabled, retry the authentication after provisioning the account. This would allow us to use a `roles` claim to decide which roles to use and eg. a `school` claim to determine a specific instance. We may use https://github.com/PerimeterX/marshmallow to parse the RegisteredClaims and get the custom claims as a separate map. +Here the `resource` takes the instance domain URI, but an `acct:` URI works as well. -For now, webfinger can only match users based on a regex and produce a list of instances based on that. +## Authenticated Instance Discovery -Here are some Ideas which need to be discussed with all client teams in the future: +When using OpenID connect to authenticate requests clients can look up the owncloud instances a user has access to. +* Authentication is necessary to prevent leaking information about existing users +* Basic auth is not supported -### status properties - -The /.well-known/webfinger enpdoint allows us to not only get rid of some of these calls, e.g. by embedding status.php info: - -``` -{ - "subject": "acct:einstein@drive.ocis.test", - "links": [ - { - "rel": "http://openid.net/specs/connect/1.0/issuer" - "href": "https://idp.ocis.test", - }, - { - "rel": "http://webfinger.owncloud/rel/server-instance", - "href": "https://abc.drive.ocis.test" - "titles": { - "en": "Readable Instance Name" - } - "properties": { - "http://webfinger.owncloud/prop/status/maintenance": "false", - "http://webfinger.owncloud/prop/status/version": "10.0.11.3" - } - }, - { - "rel": "http://webfinger.owncloud/rel/server-instance", - "href": "https://xyz.drive.ocis.test" - "titles": { - "en": "Readable Other Instance Name" - } - "properties": { - "http://webfinger.owncloud/prop/status/maintenance": "true", - "http://webfinger.owncloud/prop/status/version": "10.0.11.2" - } - }, - ] -} -``` -### Dedicated ocis web endpoint +The default configuration will simply return the `OCIS_URL` and direct clients to that domain: -It also allows us to move some services out of a sharded deployment. We could e.g. introduce a relation for a common ocis web endpoint to not exponse the different instances in the browser bar: -``` +```json { "subject": "acct:einstein@drive.ocis.test", "links": [ { "rel": "http://openid.net/specs/connect/1.0/issuer" - "href": "https://idp.ocis.test", - }, - { - "rel": "http://webfinger.owncloud/rel/web" - "href": "https://drive.ocis.test", + "href": "https://sso.example.org/cas/oidc/", }, { "rel": "http://webfinger.owncloud/rel/server-instance", - "href": "https://abc.drive.ocis.test" + "href": "https://abc.drive.example.org" "titles": { "en": "Readable Instance Name" } }, { "rel": "http://webfinger.owncloud/rel/server-instance", - "href": "https://xyz.drive.ocis.test" + "href": "https://xyz.drive.example.org" "titles": { "en": "Readable Other Instance Name" } - }, + } ] } ``` -### Dedicated ocis web endpoint - -We could also omit the `http://webfinger.owncloud/rel/server-instance` relation and go straight for a graph service with e.g. `rel=http://libregraph.org/rel/graph`: +## Configure different instances based on OpenidConnect UserInfo claims + + +A more complex example for configuring different instances could look like this +```yaml +webfinger: + instances: + - claim: email + regex: einstein@example\.org + href: "https://{{.preferred_username}}.cloud.ocis.test" + title: + "en": "oCIS Instance for Einstein" + "de": "oCIS Instanz für Einstein" + break: true + - claim: "email" + regex: marie@example\.org + href: "https://{{.preferred_username}}.cloud.ocis.test" + title: + "en": "oCIS Instance for Marie" + "de": "oCIS Instanz für Marie" + break: false + - claim: "email" + regex: .+@example\.org + href: "https://example-org.cloud.ocis.test" + title: + "en": "oCIS Instance for example.org" + "de": "oCIS Instanz für example.org" + break: true + - claim: "email" + regex: .+@example\.com + href: "https://example-com.cloud.ocis.test" + title: + "en": "oCIS Instance for example.com" + "de": "oCIS Instanz für example.com" + break: true + - claim: "email" + regex: .+@.+\..+ + href: "https://cloud.ocis.test" + title: + "en": "oCIS Instance" + "de": "oCIS Instanz" + break: true ``` -{ - "subject": "acct:einstein@drive.ocis.test", - "links": [ - { - "rel": "http://openid.net/specs/connect/1.0/issuer" - "href": "https://idp.ocis.test", - }, - { - "rel": "http://webfinger.owncloud/rel/web" - "href": "https://drive.ocis.test", - }, - { - "rel": "http://libregraph.org/rel/graph", - "href": "https://abc.drive.ocis.test/graph/v1.0" - "titles": { - "en": "Readable Instance Name" - } - }, - ] -} -``` - -In theory the graph endpoint would allow discovering drives on any domain. But there is a lot more work to be done here. -### Subject properties +Now, an authenticated webfinger request for `acct:me@example.org` (when logged in as marie) would return two instances, based on her `email` claim, the regex matches and break flags: -We could also embed subject metadata, however since apps like ocis web also need the groups a user is member of a dedicated call to the libregraph api is probably better. In any case, we could return properties for the subject: -``` +```json { - "subject": "acct:einstein@drive.ocis.test", - "properties": { - "http://libregraph.org/prop/user/id": "4c510ada-c86b-4815-8820-42cdf82c3d51", - "http://libregraph.org/prop/user/onPremisesSamAccountName": "einstein", - "http://libregraph.org/prop/user/mail": "einstein@example.org", - "http://libregraph.org/prop/user/displayName": "Albert Einstein", - } + "subject": "acct:marie@example.org", "links": [ { "rel": "http://openid.net/specs/connect/1.0/issuer" - "href": "https://idp.ocis.test", + "href": "https://sso.example.org/cas/oidc/", }, { "rel": "http://webfinger.owncloud/rel/server-instance", - "href": "https://abc.drive.ocis.test" - "titles": { - "en": "Readable Instance Name" - } + "href": "https://marie.cloud.ocis.test", + "titles": { + "en": "oCIS Instance for Marie", + "de": "oCIS Instanz für Marie" + } }, { "rel": "http://webfinger.owncloud/rel/server-instance", - "href": "https://xyz.drive.ocis.test" - "titles": { - "en": "Readable Other Instance Name" - } - }, - ] -} -``` - -# status php - -``` -{ - "subject": "https://drive.ocis.test", - "properties": { - "http://webfinger.owncloud/prop/maintenance": "false", - "http://webfinger.owncloud/prop/version": "10.11.0.6" - } - "links": [ - { - "rel": "http://openid.net/specs/connect/1.0/issuer" - "href": "https://idp.ocis.test", - }, + "href": "https://xyz.drive.example.org", + "titles": { + "en": "oCIS Instance for example.org", + "de": "oCIS Instanz für example.org" + } + } ] } ``` \ No newline at end of file diff --git a/services/webfinger/pkg/config/config.go b/services/webfinger/pkg/config/config.go index 51c94e1f9ed..cedbdb2d0ab 100644 --- a/services/webfinger/pkg/config/config.go +++ b/services/webfinger/pkg/config/config.go @@ -29,8 +29,8 @@ type Config struct { // Instance to use with a matching rule and titles type Instance struct { Claim string `yaml:"claim"` - Regex string `yaml:"rule"` + Regex string `yaml:"regex"` Href string `yaml:"href"` - Titles map[string]string `yaml:"title"` + Titles map[string]string `yaml:"titles"` Break bool `yaml:"break"` } diff --git a/services/webfinger/pkg/config/defaults/defaultconfig.go b/services/webfinger/pkg/config/defaults/defaultconfig.go index d160157d18c..f7765781b00 100644 --- a/services/webfinger/pkg/config/defaults/defaultconfig.go +++ b/services/webfinger/pkg/config/defaults/defaultconfig.go @@ -17,14 +17,12 @@ func FullDefaultConfig() *config.Config { func DefaultConfig() *config.Config { return &config.Config{ Debug: config.Debug{ - //Addr: "127.0.0.1:19119", // FIXME Addr: "127.0.0.1:0", // :0 to pick any free local port Token: "", Pprof: false, Zpages: false, }, HTTP: config.HTTP{ - //Addr: "127.0.0.1:19115", // FIXME Addr: "127.0.0.1:0", // :0 to pick any free local port Root: "/", Namespace: "com.owncloud.web", @@ -39,52 +37,11 @@ func DefaultConfig() *config.Config { Relations: []string{relations.OpenIDConnectRel, relations.OwnCloudInstanceRel}, Instances: []config.Instance{ { - Claim: "email", - Regex: "einstein@example\\.org", // only einstein - Href: "{{.OCIS_URL}}", - Titles: map[string]string{ - "en": "oCIS Instance for Einstein", - "de": "oCIS Instanz für Einstein", - }, - Break: true, - }, - { - Claim: "email", - Regex: "marie@example\\.org", // only marie - Href: "https://{{.preferred_username}}.cloud.ocis.test", - Titles: map[string]string{ - "en": "oCIS Instance for Marie", - "de": "oCIS Instanz für Marie", - }, - // also continue with next instance - }, - { - Claim: "email", - Regex: ".+@example\\.org", // example.org, including marie but not for einstein - Href: "{{.OCIS_URL}}", // zb https://{{schoolid}}.cloud.ocis.de bei dem der schoolid claim dann genommen wird. templates? - Titles: map[string]string{ - "en": "oCIS Instance for example.org", - "de": "oCIS Instanz für example.org", - }, - Break: true, - }, - { - Claim: "email", - Regex: ".+@example\\.com", // example.com - Href: "{{.OCIS_URL}}", - Titles: map[string]string{ - "en": "oCIS Instance for example.com", - "de": "oCIS Instanz für example.com", - }, - Break: true, - }, - { - Claim: "email", + Claim: "sub", Regex: ".+", Href: "{{.OCIS_URL}}", Titles: map[string]string{ "en": "oCIS Instance", - "de": "oCIS Instanz", }, }, }, @@ -125,5 +82,4 @@ func Sanitize(cfg *config.Config) { if cfg.HTTP.Root != "/" { cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/") } - } diff --git a/services/webfinger/pkg/server/http/server.go b/services/webfinger/pkg/server/http/server.go index df3f6c488c4..c263f35223e 100644 --- a/services/webfinger/pkg/server/http/server.go +++ b/services/webfinger/pkg/server/http/server.go @@ -59,14 +59,9 @@ func Server(opts ...Option) (ohttp.Service, error) { version.String, )) - // FIXME urgh we would have to do the auth ourself ... - // use auth bearer service ... - - // TODO use plain oidc claims: we don't want to have to call reva, which makes a call to ldap and also fetches groups ... - // - mux.Use(middleware.OidcAuth( middleware.WithLogger(options.Logger), + middleware.WithOidcIssuer(options.Config.IDP), )) // this logs http request related data From 492a4095add6c2613dade3ddd228b9e0a6e8cda9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Thu, 9 Feb 2023 21:58:39 +0000 Subject: [PATCH 11/23] readme cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- services/webfinger/README.md | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/services/webfinger/README.md b/services/webfinger/README.md index 896042e9983..0a305f49d98 100644 --- a/services/webfinger/README.md +++ b/services/webfinger/README.md @@ -7,13 +7,13 @@ It is based on https://github.com/owncloud/lookup-webfinger-sciebo but also retu ## OpenID Connect Discovery Clients can make an unauthenticated `GET https://drive.ocis.test/.well-known/webfinger?resource=https%3A%2F%2Fcloud.ocis.test` request to discover the OpenID Connect Issuer in the `http://openid.net/specs/connect/1.0/issuer` relation: -``` +```json { "subject": "acct:einstein@drive.ocis.test", "links": [ { - "rel": "http://openid.net/specs/connect/1.0/issuer" - "href": "https://sso.example.org/cas/oidc/", + "rel": "http://openid.net/specs/connect/1.0/issuer", + "href": "https://sso.example.org/cas/oidc/" } ] } @@ -27,7 +27,6 @@ When using OpenID connect to authenticate requests clients can look up the owncl * Authentication is necessary to prevent leaking information about existing users * Basic auth is not supported - The default configuration will simply return the `OCIS_URL` and direct clients to that domain: ```json @@ -35,22 +34,15 @@ The default configuration will simply return the `OCIS_URL` and direct clients t "subject": "acct:einstein@drive.ocis.test", "links": [ { - "rel": "http://openid.net/specs/connect/1.0/issuer" - "href": "https://sso.example.org/cas/oidc/", + "rel": "http://openid.net/specs/connect/1.0/issuer", + "href": "https://sso.example.org/cas/oidc/" }, { "rel": "http://webfinger.owncloud/rel/server-instance", - "href": "https://abc.drive.example.org" - "titles": { - "en": "Readable Instance Name" - } - }, - { - "rel": "http://webfinger.owncloud/rel/server-instance", - "href": "https://xyz.drive.example.org" - "titles": { - "en": "Readable Other Instance Name" - } + "href": "https://abc.drive.example.org", + "titles": { + "en": "oCIS Instance" + } } ] } @@ -58,7 +50,6 @@ The default configuration will simply return the `OCIS_URL` and direct clients t ## Configure different instances based on OpenidConnect UserInfo claims - A more complex example for configuring different instances could look like this ```yaml webfinger: @@ -107,8 +98,8 @@ Now, an authenticated webfinger request for `acct:me@example.org` (when logged i "subject": "acct:marie@example.org", "links": [ { - "rel": "http://openid.net/specs/connect/1.0/issuer" - "href": "https://sso.example.org/cas/oidc/", + "rel": "http://openid.net/specs/connect/1.0/issuer", + "href": "https://sso.example.org/cas/oidc/" }, { "rel": "http://webfinger.owncloud/rel/server-instance", From bf38987a744054710555b6e56ac820de11139914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Thu, 9 Feb 2023 22:00:49 +0000 Subject: [PATCH 12/23] add TODO.md with ideas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- services/webfinger/TODO.md | 138 +++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 services/webfinger/TODO.md diff --git a/services/webfinger/TODO.md b/services/webfinger/TODO.md new file mode 100644 index 00000000000..8a960007ffd --- /dev/null +++ b/services/webfinger/TODO.md @@ -0,0 +1,138 @@ + +# TODO +Currently, clients need to make subsequent calls to: +* /status.php to check if the instance is in maintenance mode or if the version is supported +* /config.json to get the available apps for ocis web to determine which routes require authentication +* /themes/owncloud/theme.json for theming info +* /.well-known/openid-configuration, auth2 token and oidc userinfo endpoints to authenticate the user +* /ocs/v1.php/cloud/user to get the username, eg. einstein ... again? it contains the oc10 user id (marie, not the uuid) +* /ocs/v1.php/cloud/capabilities to fetch instance capabilites +* /ocs/v1.php/cloud/users/einstein to fetch the quota which could come from graph and actually is now tied to the spaces, not to users +* /graph/v1.0/me?%24expand=memberOf to fetch the user id and the groups the user is a member of + +We need a way to pass oidc claims from the proxy, which does the authentication to the webfinger service, preferably by minting them into the internal reva token. +- Currently, we use machine auth so we can autoprovision an account if it does not exist. We should use revas oidc auth and, when autoprovisioning is enabled, retry the authentication after provisioning the account. This would allow us to use a `roles` claim to decide which roles to use and eg. a `school` claim to determine a specific instance. We may use https://github.com/PerimeterX/marshmallow to parse the RegisteredClaims and get the custom claims as a separate map. + +For now, webfinger can only match users based on a regex and produce a list of instances based on that. + +Here are some Ideas which need to be discussed with all client teams in the future: + +## Implement a backend lookup + +We could use ldap, the graph service or a reva based authentication to look up more properties that can be used to determine which instances to list. The initial implementation works on oidc claims and does not work with basic auth. + +## Replace status.php with properties + +The /.well-known/webfinger enpdoint allows us to not only get rid of some of these calls, e.g. by embedding status.php info: + +``` +{ + "subject": "https://drive.ocis.test", + "properties": { + "http://webfinger.owncloud/prop/maintenance": "false", + "http://webfinger.owncloud/prop/version": "10.11.0.6" + } + "links": [ + { + "rel": "http://openid.net/specs/connect/1.0/issuer" + "href": "https://idp.ocis.test", + }, + ] +} +``` + +## Introduce Dedicated ocis web endpoint + +It also allows us to move some services out of a sharded deployment. We could e.g. introduce a relation for a common ocis web endpoint to not exponse the different instances in the browser bar: +``` +{ + "subject": "acct:einstein@drive.ocis.test", + "links": [ + { + "rel": "http://openid.net/specs/connect/1.0/issuer" + "href": "https://idp.ocis.test", + }, + { + "rel": "http://webfinger.owncloud/rel/web" + "href": "https://drive.ocis.test", + }, + { + "rel": "http://webfinger.owncloud/rel/server-instance", + "href": "https://abc.drive.ocis.test" + "titles": { + "en": "Readable Instance Name" + } + }, + { + "rel": "http://webfinger.owncloud/rel/server-instance", + "href": "https://xyz.drive.ocis.test" + "titles": { + "en": "Readable Other Instance Name" + } + }, + ] +} +``` + +## Dedicated ocis web endpoint + +We could also omit the `http://webfinger.owncloud/rel/server-instance` relation and go straight for a graph service with e.g. `rel=http://libregraph.org/rel/graph`: +``` +{ + "subject": "acct:einstein@drive.ocis.test", + "links": [ + { + "rel": "http://openid.net/specs/connect/1.0/issuer" + "href": "https://idp.ocis.test", + }, + { + "rel": "http://webfinger.owncloud/rel/web" + "href": "https://drive.ocis.test", + }, + { + "rel": "http://libregraph.org/rel/graph", + "href": "https://abc.drive.ocis.test/graph/v1.0" + "titles": { + "en": "Readable Instance Name" + } + }, + ] +} +``` + +In theory the graph endpoint would allow discovering drives on any domain. But there is a lot more work to be done here. + +## Subject properties + +We could also embed subject metadata, however since apps like ocis web also need the groups a user is member of a dedicated call to the libregraph api is probably better. In any case, we could return properties for the subject: +``` +{ + "subject": "acct:einstein@drive.ocis.test", + "properties": { + "http://libregraph.org/prop/user/id": "4c510ada-c86b-4815-8820-42cdf82c3d51", + "http://libregraph.org/prop/user/onPremisesSamAccountName": "einstein", + "http://libregraph.org/prop/user/mail": "einstein@example.org", + "http://libregraph.org/prop/user/displayName": "Albert Einstein", + } + "links": [ + { + "rel": "http://openid.net/specs/connect/1.0/issuer" + "href": "https://idp.ocis.test", + }, + { + "rel": "http://webfinger.owncloud/rel/server-instance", + "href": "https://abc.drive.ocis.test" + "titles": { + "en": "Readable Instance Name" + } + }, + { + "rel": "http://webfinger.owncloud/rel/server-instance", + "href": "https://xyz.drive.ocis.test" + "titles": { + "en": "Readable Other Instance Name" + } + }, + ] +} +``` From 1aa70d2aa2d89474f789c5082c861ac033f32568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 10 Feb 2023 09:11:33 +0000 Subject: [PATCH 13/23] replace subject on authenticated request responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- .../pkg/relations/owncloud-instance.go | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/services/webfinger/pkg/relations/owncloud-instance.go b/services/webfinger/pkg/relations/owncloud-instance.go index e2e2c6ebcd1..e588c92e209 100644 --- a/services/webfinger/pkg/relations/owncloud-instance.go +++ b/services/webfinger/pkg/relations/owncloud-instance.go @@ -2,6 +2,7 @@ package relations import ( "context" + "net/url" "regexp" "strings" "text/template" @@ -23,8 +24,9 @@ type compiledInstance struct { } type ownCloudInstance struct { - instances []compiledInstance - ocisURL string + instances []compiledInstance + ocisURL string + instanceHost string } func OwnCloudInstance(instances []config.Instance, ocisURL string) (service.RelationProvider, error) { @@ -43,9 +45,14 @@ func OwnCloudInstance(instances []config.Instance, ocisURL string) (service.Rela compiledInstances = append(compiledInstances, compiled) } + u, err := url.Parse(ocisURL) + if err != nil { + return nil, err + } return &ownCloudInstance{ - instances: compiledInstances, - ocisURL: ocisURL, + instances: compiledInstances, + ocisURL: ocisURL, + instanceHost: u.Host + u.Path, }, nil } @@ -54,6 +61,11 @@ func (l *ownCloudInstance) Add(ctx context.Context, jrd *webfinger.JSONResourceD jrd = &webfinger.JSONResourceDescriptor{} } if claims := oidc.FromContext(ctx); claims != nil { + if value, ok := claims[oidc.PreferredUsername].(string); ok { + jrd.Subject = "acct:" + value + "@" + l.instanceHost + } else if value, ok := claims[oidc.Email].(string); ok { + jrd.Subject = "mailto:" + value + } // allow referencing OCIS_URL in the template claims["OCIS_URL"] = l.ocisURL for _, instance := range l.instances { From 05b5be483fb3dabe09e5d56c76295a915f9704c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 10 Feb 2023 10:20:40 +0100 Subject: [PATCH 14/23] Apply suggestions from code review Co-authored-by: Martin --- services/webfinger/README.md | 12 ++++++------ services/webfinger/TODO.md | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/services/webfinger/README.md b/services/webfinger/README.md index 0a305f49d98..f94316642ab 100644 --- a/services/webfinger/README.md +++ b/services/webfinger/README.md @@ -1,4 +1,4 @@ -# Webfinger service +# Webfinger Service The webfinger service provides an RFC7033 WebFinger lookup of ownCloud instances relevant for a given user account. @@ -19,13 +19,13 @@ Clients can make an unauthenticated `GET https://drive.ocis.test/.well-known/web } ``` -Here the `resource` takes the instance domain URI, but an `acct:` URI works as well. +Here, the `resource` takes the instance domain URI, but an `acct:` URI works as well. ## Authenticated Instance Discovery -When using OpenID connect to authenticate requests clients can look up the owncloud instances a user has access to. -* Authentication is necessary to prevent leaking information about existing users -* Basic auth is not supported +When using OpenID connect to authenticate requests, clients can look up the owncloud instances a user has access to. +* Authentication is necessary to prevent leaking information about existing users. +* Basic auth is not supported. The default configuration will simply return the `OCIS_URL` and direct clients to that domain: @@ -50,7 +50,7 @@ The default configuration will simply return the `OCIS_URL` and direct clients t ## Configure different instances based on OpenidConnect UserInfo claims -A more complex example for configuring different instances could look like this +A more complex example for configuring different instances could look like this: ```yaml webfinger: instances: diff --git a/services/webfinger/TODO.md b/services/webfinger/TODO.md index 8a960007ffd..3993b147f73 100644 --- a/services/webfinger/TODO.md +++ b/services/webfinger/TODO.md @@ -17,11 +17,11 @@ For now, webfinger can only match users based on a regex and produce a list of i Here are some Ideas which need to be discussed with all client teams in the future: -## Implement a backend lookup +## Implement a Backend Lookup We could use ldap, the graph service or a reva based authentication to look up more properties that can be used to determine which instances to list. The initial implementation works on oidc claims and does not work with basic auth. -## Replace status.php with properties +## Replace status.php with Properties The /.well-known/webfinger enpdoint allows us to not only get rid of some of these calls, e.g. by embedding status.php info: @@ -41,7 +41,7 @@ The /.well-known/webfinger enpdoint allows us to not only get rid of some of the } ``` -## Introduce Dedicated ocis web endpoint +## Introduce Dedicated ocis web Endpoint It also allows us to move some services out of a sharded deployment. We could e.g. introduce a relation for a common ocis web endpoint to not exponse the different instances in the browser bar: ``` @@ -74,7 +74,7 @@ It also allows us to move some services out of a sharded deployment. We could e. } ``` -## Dedicated ocis web endpoint +## Dedicated ocis web Endpoint We could also omit the `http://webfinger.owncloud/rel/server-instance` relation and go straight for a graph service with e.g. `rel=http://libregraph.org/rel/graph`: ``` @@ -102,7 +102,7 @@ We could also omit the `http://webfinger.owncloud/rel/server-instance` relation In theory the graph endpoint would allow discovering drives on any domain. But there is a lot more work to be done here. -## Subject properties +## Subject Properties We could also embed subject metadata, however since apps like ocis web also need the groups a user is member of a dedicated call to the libregraph api is probably better. In any case, we could return properties for the subject: ``` From c00afed152ceb684c39862caf8195348b7d8c31e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 10 Feb 2023 09:24:27 +0000 Subject: [PATCH 15/23] markdown lint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- services/webfinger/TODO.md | 40 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/services/webfinger/TODO.md b/services/webfinger/TODO.md index 3993b147f73..cf769811984 100644 --- a/services/webfinger/TODO.md +++ b/services/webfinger/TODO.md @@ -25,18 +25,18 @@ We could use ldap, the graph service or a reva based authentication to look up m The /.well-known/webfinger enpdoint allows us to not only get rid of some of these calls, e.g. by embedding status.php info: -``` +```json { "subject": "https://drive.ocis.test", "properties": { "http://webfinger.owncloud/prop/maintenance": "false", "http://webfinger.owncloud/prop/version": "10.11.0.6" - } + }, "links": [ { - "rel": "http://openid.net/specs/connect/1.0/issuer" - "href": "https://idp.ocis.test", - }, + "rel": "http://openid.net/specs/connect/1.0/issuer", + "href": "https://idp.ocis.test" + } ] } ``` @@ -44,32 +44,32 @@ The /.well-known/webfinger enpdoint allows us to not only get rid of some of the ## Introduce Dedicated ocis web Endpoint It also allows us to move some services out of a sharded deployment. We could e.g. introduce a relation for a common ocis web endpoint to not exponse the different instances in the browser bar: -``` +```json { "subject": "acct:einstein@drive.ocis.test", "links": [ { - "rel": "http://openid.net/specs/connect/1.0/issuer" - "href": "https://idp.ocis.test", + "rel": "http://openid.net/specs/connect/1.0/issuer", + "href": "https://idp.ocis.test" }, { - "rel": "http://webfinger.owncloud/rel/web" - "href": "https://drive.ocis.test", + "rel": "http://webfinger.owncloud/rel/web", + "href": "https://drive.ocis.test" }, { "rel": "http://webfinger.owncloud/rel/server-instance", - "href": "https://abc.drive.ocis.test" + "href": "https://abc.drive.ocis.test", "titles": { "en": "Readable Instance Name" } }, { "rel": "http://webfinger.owncloud/rel/server-instance", - "href": "https://xyz.drive.ocis.test" + "href": "https://xyz.drive.ocis.test", "titles": { "en": "Readable Other Instance Name" } - }, + } ] } ``` @@ -77,25 +77,25 @@ It also allows us to move some services out of a sharded deployment. We could e. ## Dedicated ocis web Endpoint We could also omit the `http://webfinger.owncloud/rel/server-instance` relation and go straight for a graph service with e.g. `rel=http://libregraph.org/rel/graph`: -``` +```json { "subject": "acct:einstein@drive.ocis.test", "links": [ { - "rel": "http://openid.net/specs/connect/1.0/issuer" - "href": "https://idp.ocis.test", + "rel": "http://openid.net/specs/connect/1.0/issuer", + "href": "https://idp.ocis.test" }, { - "rel": "http://webfinger.owncloud/rel/web" - "href": "https://drive.ocis.test", + "rel": "http://webfinger.owncloud/rel/web", + "href": "https://drive.ocis.test" }, { "rel": "http://libregraph.org/rel/graph", - "href": "https://abc.drive.ocis.test/graph/v1.0" + "href": "https://abc.drive.ocis.test/graph/v1.0", "titles": { "en": "Readable Instance Name" } - }, + } ] } ``` From 1fb4e9d2bc300f8e1cfd82cff6c0e2699ac111f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 10 Feb 2023 11:26:16 +0000 Subject: [PATCH 16/23] return a 401 when bearer token expired, some more docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- ocis-pkg/middleware/oidc.go | 5 +++-- services/webfinger/TODO.md | 15 +++++++-------- .../webfinger/pkg/relations/openid-discovery.go | 3 +-- .../webfinger/pkg/relations/owncloud-instance.go | 1 + 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ocis-pkg/middleware/oidc.go b/ocis-pkg/middleware/oidc.go index c6bcefbaea1..2c1dd5b194d 100644 --- a/ocis-pkg/middleware/oidc.go +++ b/ocis-pkg/middleware/oidc.go @@ -69,8 +69,9 @@ func OidcAuth(opts ...Option) func(http.Handler) http.Handler { oauth2.StaticTokenSource(oauth2Token), ) if err != nil { - // what if unauthorized - break + w.Header().Add("WWW-Authenticate", `Bearer`) + w.WriteHeader(http.StatusUnauthorized) + return } claims := map[string]interface{}{} err = userInfo.Claims(&claims) diff --git a/services/webfinger/TODO.md b/services/webfinger/TODO.md index cf769811984..866ba6f4858 100644 --- a/services/webfinger/TODO.md +++ b/services/webfinger/TODO.md @@ -1,4 +1,3 @@ - # TODO Currently, clients need to make subsequent calls to: * /status.php to check if the instance is in maintenance mode or if the version is supported @@ -11,7 +10,7 @@ Currently, clients need to make subsequent calls to: * /graph/v1.0/me?%24expand=memberOf to fetch the user id and the groups the user is a member of We need a way to pass oidc claims from the proxy, which does the authentication to the webfinger service, preferably by minting them into the internal reva token. -- Currently, we use machine auth so we can autoprovision an account if it does not exist. We should use revas oidc auth and, when autoprovisioning is enabled, retry the authentication after provisioning the account. This would allow us to use a `roles` claim to decide which roles to use and eg. a `school` claim to determine a specific instance. We may use https://github.com/PerimeterX/marshmallow to parse the RegisteredClaims and get the custom claims as a separate map. +* Currently, we use machine auth so we can autoprovision an account if it does not exist. We should use revas oidc auth and, when autoprovisioning is enabled, retry the authentication after provisioning the account. This would allow us to use a `roles` claim to decide which roles to use and eg. a `school` claim to determine a specific instance. We may use https://github.com/PerimeterX/marshmallow to parse the RegisteredClaims and get the custom claims as a separate map. For now, webfinger can only match users based on a regex and produce a list of instances based on that. @@ -105,7 +104,7 @@ In theory the graph endpoint would allow discovering drives on any domain. But t ## Subject Properties We could also embed subject metadata, however since apps like ocis web also need the groups a user is member of a dedicated call to the libregraph api is probably better. In any case, we could return properties for the subject: -``` +```json { "subject": "acct:einstein@drive.ocis.test", "properties": { @@ -113,22 +112,22 @@ We could also embed subject metadata, however since apps like ocis web also need "http://libregraph.org/prop/user/onPremisesSamAccountName": "einstein", "http://libregraph.org/prop/user/mail": "einstein@example.org", "http://libregraph.org/prop/user/displayName": "Albert Einstein", - } + }, "links": [ { - "rel": "http://openid.net/specs/connect/1.0/issuer" - "href": "https://idp.ocis.test", + "rel": "http://openid.net/specs/connect/1.0/issuer", + "href": "https://idp.ocis.test" }, { "rel": "http://webfinger.owncloud/rel/server-instance", - "href": "https://abc.drive.ocis.test" + "href": "https://abc.drive.ocis.test", "titles": { "en": "Readable Instance Name" } }, { "rel": "http://webfinger.owncloud/rel/server-instance", - "href": "https://xyz.drive.ocis.test" + "href": "https://xyz.drive.ocis.test", "titles": { "en": "Readable Other Instance Name" } diff --git a/services/webfinger/pkg/relations/openid-discovery.go b/services/webfinger/pkg/relations/openid-discovery.go index 86fe898df1a..43bae7f9b95 100644 --- a/services/webfinger/pkg/relations/openid-discovery.go +++ b/services/webfinger/pkg/relations/openid-discovery.go @@ -15,6 +15,7 @@ type openIDDiscovery struct { Href string } +// OpenIDDiscovery adds the Openid Connect issuer relation func OpenIDDiscovery(href string) service.RelationProvider { return &openIDDiscovery{ Href: href, @@ -25,10 +26,8 @@ func (l *openIDDiscovery) Add(ctx context.Context, jrd *webfinger.JSONResourceDe if jrd == nil { jrd = &webfinger.JSONResourceDescriptor{} } - // TODO check if this relation was requested jrd.Links = append(jrd.Links, webfinger.Link{ Rel: OpenIDConnectRel, Href: l.Href, - // Titles: , // TODO use , separated env var with : separated language -> title pairs }) } diff --git a/services/webfinger/pkg/relations/owncloud-instance.go b/services/webfinger/pkg/relations/owncloud-instance.go index e588c92e209..f70f5407b02 100644 --- a/services/webfinger/pkg/relations/owncloud-instance.go +++ b/services/webfinger/pkg/relations/owncloud-instance.go @@ -29,6 +29,7 @@ type ownCloudInstance struct { instanceHost string } +// OwnCloudInstance adds one or more ownCloud instance relations func OwnCloudInstance(instances []config.Instance, ocisURL string) (service.RelationProvider, error) { compiledInstances := make([]compiledInstance, 0, len(instances)) var err error From 5037144bec0567e76cf91439c3b0874b69361446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 10 Feb 2023 12:28:28 +0100 Subject: [PATCH 17/23] Apply suggestions from code review Co-authored-by: Martin --- services/webfinger/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/webfinger/README.md b/services/webfinger/README.md index f94316642ab..1ba4d95fcb4 100644 --- a/services/webfinger/README.md +++ b/services/webfinger/README.md @@ -48,7 +48,7 @@ The default configuration will simply return the `OCIS_URL` and direct clients t } ``` -## Configure different instances based on OpenidConnect UserInfo claims +## Configure Different Instances Based on OpenidConnect UserInfo Claims A more complex example for configuring different instances could look like this: ```yaml From 1e013c8e6baa16b27a81f3c084a1faf39971f0bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 10 Feb 2023 11:31:13 +0000 Subject: [PATCH 18/23] fix docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- docs/services/webfinger/_index.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/services/webfinger/_index.md diff --git a/docs/services/webfinger/_index.md b/docs/services/webfinger/_index.md new file mode 100644 index 00000000000..178f0c84d76 --- /dev/null +++ b/docs/services/webfinger/_index.md @@ -0,0 +1,18 @@ +--- +title: Webfinger +date: 2023-02-03T00:00:00+00:00 +weight: 20 +geekdocRepo: https://github.com/owncloud/ocis +geekdocEditPath: edit/master/docs/services/webfinger +geekdocFilePath: _index.md +geekdocCollapseSection: true +--- + +## Abstract + +This service provides endpoints a the /.well-known/webfinger implementation. + +## Table of Contents + +{{< toc-tree >}} + From e947e829adc0c9b8135ed17843595ed265b47393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 10 Feb 2023 13:37:08 +0000 Subject: [PATCH 19/23] clarify env var MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- services/webfinger/pkg/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/webfinger/pkg/config/config.go b/services/webfinger/pkg/config/config.go index cedbdb2d0ab..66a1b424f64 100644 --- a/services/webfinger/pkg/config/config.go +++ b/services/webfinger/pkg/config/config.go @@ -21,7 +21,7 @@ type Config struct { Instances []Instance `yaml:"instances"` Relations []string `yaml:"relations" env:"WEBFINGER_RELATIONS" desc:"A comma-separated list of relation URIs or registered relation types to add to webfinger responses."` IDP string `yaml:"idp" env:"OCIS_URL;OCIS_OIDC_ISSUER;WEBFINGER_OIDC_ISSUER" desc:"The identity provider href for the openid-discovery relation."` - OcisURL string `yaml:"ocis_url" env:"OCIS_URL;WEBFINGER_OCIS_URL" desc:"URL, where oCIS is reachable for users."` + OcisURL string `yaml:"ocis_url" env:"OCIS_URL;WEBFINGER_OWNCLOUD_SERVER_INSTANCE_URL" desc:"The URL for the owncloud server instance relation."` Context context.Context `yaml:"-"` } From 7f6d7023fce1efd5e79f70e94f080f7150105bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 10 Feb 2023 13:52:52 +0000 Subject: [PATCH 20/23] extract handler func MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- services/webfinger/pkg/server/http/server.go | 96 ++++++++++---------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/services/webfinger/pkg/server/http/server.go b/services/webfinger/pkg/server/http/server.go index c263f35223e..ae7af703c38 100644 --- a/services/webfinger/pkg/server/http/server.go +++ b/services/webfinger/pkg/server/http/server.go @@ -12,6 +12,7 @@ import ( ohttp "github.com/owncloud/ocis/v2/ocis-pkg/service/http" "github.com/owncloud/ocis/v2/ocis-pkg/version" serviceErrors "github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0" + svc "github.com/owncloud/ocis/v2/services/webfinger/pkg/service/v0" "github.com/pkg/errors" "go-micro.dev/v4" ) @@ -70,52 +71,7 @@ func Server(opts ...Option) (ohttp.Service, error) { )) mux.Route(options.Config.HTTP.Root, func(r chi.Router) { - - r.Get("/.well-known/webfinger", func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - // A WebFinger URI MUST contain a query component (see Section 3.4 of - // RFC 3986). The query component MUST contain a "resource" parameter - // and MAY contain one or more "rel" parameters. - resource := r.URL.Query().Get("resource") - queryTarget, err := url.Parse(resource) - if resource == "" || err != nil { - // If the "resource" parameter is absent or malformed, the WebFinger - // resource MUST indicate that the request is bad as per Section 10.4.1 - // of RFC 2616. - render.Status(r, http.StatusBadRequest) - render.PlainText(w, r, "absent or malformed 'resource' parameter") - return - } - - rels := make([]string, 0) - for k, v := range r.URL.Query() { - if k == "rel" { - rels = append(rels, v...) - } - } - - jrd, err := service.Webfinger(ctx, queryTarget, rels) - if errors.Is(err, serviceErrors.ErrNotFound) { - // from https://www.rfc-editor.org/rfc/rfc7033#section-4.2 - // - // If the "resource" parameter is a value for which the server has no - // information, the server MUST indicate that it was unable to match the - // request as per Section 10.4.5 of RFC 2616. - render.Status(r, http.StatusNotFound) - render.PlainText(w, r, err.Error()) - return - } - if err != nil { - render.Status(r, http.StatusInternalServerError) - render.PlainText(w, r, err.Error()) - return - } - - w.Header().Set("Content-type", "application/jrd+json") - render.Status(r, http.StatusOK) - render.JSON(w, r, jrd) - }) + r.Get("/.well-known/webfinger", WebfingerHandler(service)) }) err = micro.RegisterHandler(svc.Server(), mux) @@ -126,3 +82,51 @@ func Server(opts ...Option) (ohttp.Service, error) { svc.Init() return svc, nil } + +func WebfingerHandler(service svc.Service) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // A WebFinger URI MUST contain a query component (see Section 3.4 of + // RFC 3986). The query component MUST contain a "resource" parameter + // and MAY contain one or more "rel" parameters. + resource := r.URL.Query().Get("resource") + queryTarget, err := url.Parse(resource) + if resource == "" || err != nil { + // If the "resource" parameter is absent or malformed, the WebFinger + // resource MUST indicate that the request is bad as per Section 10.4.1 + // of RFC 2616. + render.Status(r, http.StatusBadRequest) + render.PlainText(w, r, "absent or malformed 'resource' parameter") + return + } + + rels := make([]string, 0) + for k, v := range r.URL.Query() { + if k == "rel" { + rels = append(rels, v...) + } + } + + jrd, err := service.Webfinger(ctx, queryTarget, rels) + if errors.Is(err, serviceErrors.ErrNotFound) { + // from https://www.rfc-editor.org/rfc/rfc7033#section-4.2 + // + // If the "resource" parameter is a value for which the server has no + // information, the server MUST indicate that it was unable to match the + // request as per Section 10.4.5 of RFC 2616. + render.Status(r, http.StatusNotFound) + render.PlainText(w, r, err.Error()) + return + } + if err != nil { + render.Status(r, http.StatusInternalServerError) + render.PlainText(w, r, err.Error()) + return + } + + w.Header().Set("Content-type", "application/jrd+json") + render.Status(r, http.StatusOK) + render.JSON(w, r, jrd) + } +} From abb42c1510c6ddfd2cc3cc57445fb5eb706711fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 10 Feb 2023 13:53:18 +0000 Subject: [PATCH 21/23] use correct service in reflex.conf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- services/webfinger/reflex.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/webfinger/reflex.conf b/services/webfinger/reflex.conf index 32c48e4c467..a344439d5f2 100644 --- a/services/webfinger/reflex.conf +++ b/services/webfinger/reflex.conf @@ -1,2 +1,2 @@ # backend --r '^(cmd|pkg)/.*\.go$' -R '^node_modules/' -s -- sh -c 'make bin/ocis-graph-debug && bin/ocis-graph-debug --log-level debug server --debug-pprof --debug-zpages --oidc-endpoint="https://deepdiver" --oidc-insecure=1' +-r '^(cmd|pkg)/.*\.go$' -R '^node_modules/' -s -- sh -c 'make bin/ocis-webfinger-debug && bin/ocis-webfinger-debug --log-level debug server --debug-pprof --debug-zpages' From a4741da6147a68a51e9e1b601d0a73b37c81fedf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 10 Feb 2023 14:31:26 +0000 Subject: [PATCH 22/23] test relations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- ...penid-discovery.go => openid_discovery.go} | 0 .../pkg/relations/openid_discovery_test.go | 26 ++++++++ ...cloud-instance.go => owncloud_instance.go} | 0 .../pkg/relations/owncloud_instance_test.go | 65 +++++++++++++++++++ 4 files changed, 91 insertions(+) rename services/webfinger/pkg/relations/{openid-discovery.go => openid_discovery.go} (100%) create mode 100644 services/webfinger/pkg/relations/openid_discovery_test.go rename services/webfinger/pkg/relations/{owncloud-instance.go => owncloud_instance.go} (100%) create mode 100644 services/webfinger/pkg/relations/owncloud_instance_test.go diff --git a/services/webfinger/pkg/relations/openid-discovery.go b/services/webfinger/pkg/relations/openid_discovery.go similarity index 100% rename from services/webfinger/pkg/relations/openid-discovery.go rename to services/webfinger/pkg/relations/openid_discovery.go diff --git a/services/webfinger/pkg/relations/openid_discovery_test.go b/services/webfinger/pkg/relations/openid_discovery_test.go new file mode 100644 index 00000000000..33331d6f343 --- /dev/null +++ b/services/webfinger/pkg/relations/openid_discovery_test.go @@ -0,0 +1,26 @@ +package relations + +import ( + "context" + "testing" + + "github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger" +) + +func TestOpenidDiscovery(t *testing.T) { + provider := OpenIDDiscovery("http://issuer.url") + + jrd := webfinger.JSONResourceDescriptor{} + + provider.Add(context.Background(), &jrd) + + if len(jrd.Links) != 1 { + t.Errorf("provider returned wrong number of links: %v, expected 1", len(jrd.Links)) + } + if jrd.Links[0].Href != "http://issuer.url" { + t.Errorf("provider returned wrong issuer link href: %v, expected %v", jrd.Links[0].Href, "http://issuer.url") + } + if jrd.Links[0].Rel != "http://openid.net/specs/connect/1.0/issuer" { + t.Errorf("provider returned wrong openid connect rel: %v, expected %v", jrd.Links[0].Href, OpenIDConnectRel) + } +} diff --git a/services/webfinger/pkg/relations/owncloud-instance.go b/services/webfinger/pkg/relations/owncloud_instance.go similarity index 100% rename from services/webfinger/pkg/relations/owncloud-instance.go rename to services/webfinger/pkg/relations/owncloud_instance.go diff --git a/services/webfinger/pkg/relations/owncloud_instance_test.go b/services/webfinger/pkg/relations/owncloud_instance_test.go new file mode 100644 index 00000000000..891b8da0792 --- /dev/null +++ b/services/webfinger/pkg/relations/owncloud_instance_test.go @@ -0,0 +1,65 @@ +package relations + +import ( + "context" + "testing" + + "github.com/owncloud/ocis/v2/ocis-pkg/oidc" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/config" + "github.com/owncloud/ocis/v2/services/webfinger/pkg/webfinger" +) + +func TestOwnCloudInstanceErr(t *testing.T) { + _, err := OwnCloudInstance([]config.Instance{}, "http://\n\rinvalid") + if err == nil { + t.Errorf("provider did not err on invalid url: %v", err) + } + _, err = OwnCloudInstance([]config.Instance{{Regex: "("}}, "http://docis.tld") + if err == nil { + t.Errorf("provider did not err on invalid regex: %v", err) + } + _, err = OwnCloudInstance([]config.Instance{{Href: "{{invalid}}ee"}}, "http://docis.tld") + if err == nil { + t.Errorf("provider did not err on invalid href template: %v", err) + } +} + +func TestOwnCloudInstanceAddLink(t *testing.T) { + provider, err := OwnCloudInstance([]config.Instance{{ + Claim: "customclaim", + Regex: ".+@.+\\..+", + Href: "https://{{.otherclaim}}.domain.tld", + Titles: map[string]string{ + "foo": "bar", + }, + Break: true, + }}, "http://docis.tld") + if err != nil { + t.Error(err) + } + + ctx := context.Background() + ctx = oidc.NewContext(ctx, map[string]interface{}{ + "customclaim": "some@fizz.buzz", + "otherclaim": "someone", + }) + jrd := webfinger.JSONResourceDescriptor{} + provider.Add(ctx, &jrd) + + if len(jrd.Links) != 1 { + t.Errorf("provider returned wrong number of links: %v, expected 1", len(jrd.Links)) + } + if jrd.Links[0].Href != "https://someone.domain.tld" { + t.Errorf("provider returned wrong issuer link href: %v, expected %v", jrd.Links[0].Href, "https://someone.domain.tld") + } + if jrd.Links[0].Rel != OwnCloudInstanceRel { + t.Errorf("provider returned owncloud server instance rel: %v, expected %v", jrd.Links[0].Rel, OwnCloudInstanceRel) + } + if len(jrd.Links[0].Titles) != 1 { + t.Errorf("provider returned wrong number of titles: %v, expected 1", len(jrd.Links[0].Titles)) + } + if jrd.Links[0].Titles["foo"] != "bar" { + t.Errorf("provider returned wrong title: %v, expected bar", len(jrd.Links[0].Titles["foo"])) + } + +} From 8f065f20f9785bf7a8eec89b019380018a5cfcff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Mon, 13 Feb 2023 09:59:12 +0100 Subject: [PATCH 23/23] Update services/webfinger/pkg/config/config.go --- services/webfinger/pkg/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/webfinger/pkg/config/config.go b/services/webfinger/pkg/config/config.go index 66a1b424f64..aca53fdb75a 100644 --- a/services/webfinger/pkg/config/config.go +++ b/services/webfinger/pkg/config/config.go @@ -21,7 +21,7 @@ type Config struct { Instances []Instance `yaml:"instances"` Relations []string `yaml:"relations" env:"WEBFINGER_RELATIONS" desc:"A comma-separated list of relation URIs or registered relation types to add to webfinger responses."` IDP string `yaml:"idp" env:"OCIS_URL;OCIS_OIDC_ISSUER;WEBFINGER_OIDC_ISSUER" desc:"The identity provider href for the openid-discovery relation."` - OcisURL string `yaml:"ocis_url" env:"OCIS_URL;WEBFINGER_OWNCLOUD_SERVER_INSTANCE_URL" desc:"The URL for the owncloud server instance relation."` + OcisURL string `yaml:"ocis_url" env:"OCIS_URL;WEBFINGER_OWNCLOUD_SERVER_INSTANCE_URL" desc:"The URL for the legacy ownCloud server instance relation (not to be confused with the product ownCloud Server). It defaults to the OCIS_URL but can be overridden to support some reverse proxy corner cases. To shard the deployment, multiple instances can be configured in the configuration file."` Context context.Context `yaml:"-"` }