minimal webfinger (#5373)
* initial webfinger stub Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * add webfinger to proxy, return current host Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * some cleanup Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * allow passing multiple rel params Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * introduce interfaces Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * parse oidc auth token Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * add templating, drop chain, use map of relation providers Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * fix ocis url yaml Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * fix typos Co-authored-by: Dominik Schmidt <dschmidt@owncloud.com> * switch to userinfo claims Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * readme cleanup Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * add TODO.md with ideas Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * replace subject on authenticated request responses Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * Apply suggestions from code review Co-authored-by: Martin <github@diemattels.at> * markdown lint Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * return a 401 when bearer token expired, some more docs Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * Apply suggestions from code review Co-authored-by: Martin <github@diemattels.at> * fix docs Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * clarify env var Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * extract handler func Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * use correct service in reflex.conf Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * test relations Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * Update services/webfinger/pkg/config/config.go --------- Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> Co-authored-by: Dominik Schmidt <dschmidt@owncloud.com> Co-authored-by: Martin <github@diemattels.at>
This commit is contained in:
co-authored by
Dominik Schmidt
Martin
parent
139cf79f61
commit
2c98d3246c
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
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/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"
|
||||
"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)
|
||||
|
||||
{
|
||||
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.WithRelationProviders(relationProviders),
|
||||
)
|
||||
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()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
return nil, fmt.Errorf("unknown relation '%s'", relationURI)
|
||||
}
|
||||
}
|
||||
return rels, nil
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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"`
|
||||
|
||||
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 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:"-"`
|
||||
}
|
||||
|
||||
// Instance to use with a matching rule and titles
|
||||
type Instance struct {
|
||||
Claim string `yaml:"claim"`
|
||||
Regex string `yaml:"regex"`
|
||||
Href string `yaml:"href"`
|
||||
Titles map[string]string `yaml:"titles"`
|
||||
Break bool `yaml:"break"`
|
||||
}
|
||||
@@ -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."`
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/webfinger/pkg/relations"
|
||||
)
|
||||
|
||||
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:0", // :0 to pick any free local port
|
||||
Token: "",
|
||||
Pprof: false,
|
||||
Zpages: false,
|
||||
},
|
||||
HTTP: config.HTTP{
|
||||
Addr: "127.0.0.1:0", // :0 to pick any free local port
|
||||
Root: "/",
|
||||
Namespace: "com.owncloud.web",
|
||||
CORS: config.CORS{
|
||||
AllowedOrigins: []string{"*"},
|
||||
},
|
||||
},
|
||||
Service: config.Service{
|
||||
Name: "webfinger",
|
||||
},
|
||||
|
||||
Relations: []string{relations.OpenIDConnectRel, relations.OwnCloudInstanceRel},
|
||||
Instances: []config.Instance{
|
||||
{
|
||||
Claim: "sub",
|
||||
Regex: ".+",
|
||||
Href: "{{.OCIS_URL}}",
|
||||
Titles: map[string]string{
|
||||
"en": "oCIS Instance",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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, "/")
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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."`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
// Service defines the available service configuration.
|
||||
type Service struct {
|
||||
Name string `yaml:"-"`
|
||||
}
|
||||
@@ -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."`
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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
|
||||
}
|
||||
|
||||
// OpenIDDiscovery adds the Openid Connect issuer relation
|
||||
func OpenIDDiscovery(href string) service.RelationProvider {
|
||||
return &openIDDiscovery{
|
||||
Href: href,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *openIDDiscovery) Add(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) {
|
||||
if jrd == nil {
|
||||
jrd = &webfinger.JSONResourceDescriptor{}
|
||||
}
|
||||
jrd.Links = append(jrd.Links, webfinger.Link{
|
||||
Rel: OpenIDConnectRel,
|
||||
Href: l.Href,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package relations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"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
|
||||
hrefTemplate *template.Template
|
||||
}
|
||||
|
||||
type ownCloudInstance struct {
|
||||
instances []compiledInstance
|
||||
ocisURL string
|
||||
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
|
||||
for _, instance := range instances {
|
||||
compiled := compiledInstance{Instance: instance}
|
||||
compiled.compiledRegex, err = regexp.Compile(instance.Regex)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
u, err := url.Parse(ocisURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ownCloudInstance{
|
||||
instances: compiledInstances,
|
||||
ocisURL: ocisURL,
|
||||
instanceHost: u.Host + u.Path,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *ownCloudInstance) Add(ctx context.Context, jrd *webfinger.JSONResourceDescriptor) {
|
||||
if jrd == nil {
|
||||
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 {
|
||||
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: OwnCloudInstanceRel,
|
||||
Href: tmplWriter.String(),
|
||||
Titles: instance.Titles,
|
||||
})
|
||||
if instance.Break {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"]))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"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"
|
||||
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"
|
||||
)
|
||||
|
||||
// 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,
|
||||
))
|
||||
|
||||
mux.Use(middleware.OidcAuth(
|
||||
middleware.WithLogger(options.Logger),
|
||||
middleware.WithOidcIssuer(options.Config.IDP),
|
||||
))
|
||||
|
||||
// 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", WebfingerHandler(service))
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package service
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrNotFound = errors.New("query target not found")
|
||||
@@ -0,0 +1,39 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"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, queryTarget *url.URL, rels []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, queryTarget, rels)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"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, queryTarget *url.URL, rels []string) (webfinger.JSONResourceDescriptor, error) {
|
||||
l.logger.Debug().
|
||||
Str("query_target", queryTarget.String()).
|
||||
Strs("rel", rels).
|
||||
Msg("Webfinger")
|
||||
|
||||
return l.next.Webfinger(ctx, queryTarget, rels)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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
|
||||
RelationProviders map[string]RelationProvider
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
func WithRelationProviders(val map[string]RelationProvider) Option {
|
||||
return func(o *Options) {
|
||||
o.RelationProviders = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"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 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 RelationProvider interface {
|
||||
Add(ctx context.Context, jrd *webfinger.JSONResourceDescriptor)
|
||||
}
|
||||
|
||||
// New returns a new instance of Service
|
||||
func New(opts ...Option) (Service, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
// TODO use fallback implementations of InstanceIdLookup and InstanceLookup?
|
||||
// The InstanceIdLookup may have to happen earlier?
|
||||
|
||||
return svc{
|
||||
log: options.Logger,
|
||||
config: options.Config,
|
||||
relationProviders: options.RelationProviders,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type svc struct {
|
||||
config *config.Config
|
||||
log log.Logger
|
||||
relationProviders map[string]RelationProvider
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
jrd := webfinger.JSONResourceDescriptor{
|
||||
Subject: queryTarget.String(),
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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"
|
||||
"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, queryTarget *url.URL, rels []string) (webfinger.JSONResourceDescriptor, error) {
|
||||
ctx, span := webfingertracing.TraceProvider.Tracer("webfinger").Start(ctx, "Webfinger", trace.WithAttributes(
|
||||
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, queryTarget, rels)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
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"`
|
||||
// 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"
|
||||
//
|
||||
// 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"`
|
||||
}
|
||||
Reference in New Issue
Block a user