diff --git a/Makefile b/Makefile index 4d0aee176e..bd45d7972c 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,7 @@ OC_MODULES = \ services/app-provider \ services/app-registry \ services/audit \ + services/auth-api \ services/auth-app \ services/auth-basic \ services/auth-bearer \ diff --git a/opencloud/pkg/command/services.go b/opencloud/pkg/command/services.go index 9c3602f186..02cdb70ee1 100644 --- a/opencloud/pkg/command/services.go +++ b/opencloud/pkg/command/services.go @@ -13,6 +13,7 @@ import ( appprovider "github.com/opencloud-eu/opencloud/services/app-provider/pkg/command" appregistry "github.com/opencloud-eu/opencloud/services/app-registry/pkg/command" audit "github.com/opencloud-eu/opencloud/services/audit/pkg/command" + authapi "github.com/opencloud-eu/opencloud/services/auth-api/pkg/command" authapp "github.com/opencloud-eu/opencloud/services/auth-app/pkg/command" authbasic "github.com/opencloud-eu/opencloud/services/auth-basic/pkg/command" authbearer "github.com/opencloud-eu/opencloud/services/auth-bearer/pkg/command" @@ -269,6 +270,11 @@ var svccmds = []register.Command{ cfg.Webfinger.Commons = cfg.Commons }) }, + func(cfg *config.Config) *cli.Command { + return ServiceCommand(cfg, cfg.AuthApi.Service.Name, authapi.GetCommands(cfg.AuthApi), func(c *config.Config) { + cfg.AuthApi.Commons = cfg.Commons + }) + }, } // ServiceCommand is the entry point for the all service commands. diff --git a/opencloud/pkg/runtime/service/service.go b/opencloud/pkg/runtime/service/service.go index 86ad3843fd..c5ea45a3c3 100644 --- a/opencloud/pkg/runtime/service/service.go +++ b/opencloud/pkg/runtime/service/service.go @@ -24,6 +24,7 @@ import ( appProvider "github.com/opencloud-eu/opencloud/services/app-provider/pkg/command" appRegistry "github.com/opencloud-eu/opencloud/services/app-registry/pkg/command" audit "github.com/opencloud-eu/opencloud/services/audit/pkg/command" + authapi "github.com/opencloud-eu/opencloud/services/auth-api/pkg/command" authapp "github.com/opencloud-eu/opencloud/services/auth-app/pkg/command" authbasic "github.com/opencloud-eu/opencloud/services/auth-basic/pkg/command" authmachine "github.com/opencloud-eu/opencloud/services/auth-machine/pkg/command" @@ -200,11 +201,6 @@ func NewService(ctx context.Context, options ...Option) (*Service, error) { cfg.Groups.Commons = cfg.Commons return groups.Execute(cfg.Groups) }) - reg(3, opts.Config.Groupware.Service.Name, func(ctx context.Context, cfg *occfg.Config) error { - cfg.Groupware.Context = ctx - cfg.Groupware.Commons = cfg.Commons - return groupware.Execute(cfg.Groupware) - }) reg(3, opts.Config.IDM.Service.Name, func(ctx context.Context, cfg *occfg.Config) error { cfg.IDM.Context = ctx cfg.IDM.Commons = cfg.Commons @@ -354,6 +350,16 @@ func NewService(ctx context.Context, options ...Option) (*Service, error) { cfg.Notifications.Commons = cfg.Commons return notifications.Execute(cfg.Notifications) }) + areg(opts.Config.AuthApi.Service.Name, func(ctx context.Context, cfg *occfg.Config) error { + cfg.AuthApi.Context = ctx + cfg.AuthApi.Commons = cfg.Commons + return authapi.Execute(cfg.AuthApi) + }) + areg(opts.Config.Groupware.Service.Name, func(ctx context.Context, cfg *occfg.Config) error { + cfg.Groupware.Context = ctx + cfg.Groupware.Commons = cfg.Commons + return groupware.Execute(cfg.Groupware) + }) return s, nil } diff --git a/pkg/config/config.go b/pkg/config/config.go index dad2c2283a..31a3d2aa2d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -7,6 +7,7 @@ import ( appProvider "github.com/opencloud-eu/opencloud/services/app-provider/pkg/config" appRegistry "github.com/opencloud-eu/opencloud/services/app-registry/pkg/config" audit "github.com/opencloud-eu/opencloud/services/audit/pkg/config" + authapi "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config" authapp "github.com/opencloud-eu/opencloud/services/auth-app/pkg/config" authbasic "github.com/opencloud-eu/opencloud/services/auth-basic/pkg/config" authbearer "github.com/opencloud-eu/opencloud/services/auth-bearer/pkg/config" @@ -127,4 +128,5 @@ type Config struct { WebDAV *webdav.Config `yaml:"webdav"` Webfinger *webfinger.Config `yaml:"webfinger"` Search *search.Config `yaml:"search"` + AuthApi *authapi.Config `yaml:"authapi"` } diff --git a/pkg/config/defaultconfig.go b/pkg/config/defaultconfig.go index fb2f7e5f66..cd13c131ae 100644 --- a/pkg/config/defaultconfig.go +++ b/pkg/config/defaultconfig.go @@ -7,6 +7,7 @@ import ( appProvider "github.com/opencloud-eu/opencloud/services/app-provider/pkg/config/defaults" appRegistry "github.com/opencloud-eu/opencloud/services/app-registry/pkg/config/defaults" audit "github.com/opencloud-eu/opencloud/services/audit/pkg/config/defaults" + authapi "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config/defaults" authapp "github.com/opencloud-eu/opencloud/services/auth-app/pkg/config/defaults" authbasic "github.com/opencloud-eu/opencloud/services/auth-basic/pkg/config/defaults" authbearer "github.com/opencloud-eu/opencloud/services/auth-bearer/pkg/config/defaults" @@ -64,6 +65,7 @@ func DefaultConfig() *Config { AppProvider: appProvider.DefaultConfig(), AppRegistry: appRegistry.DefaultConfig(), Audit: audit.DefaultConfig(), + AuthApi: authapi.DefaultConfig(), AuthApp: authapp.DefaultConfig(), AuthBasic: authbasic.DefaultConfig(), AuthBearer: authbearer.DefaultConfig(), diff --git a/services/auth-api/Makefile b/services/auth-api/Makefile new file mode 100644 index 0000000000..fa6ce3234d --- /dev/null +++ b/services/auth-api/Makefile @@ -0,0 +1,11 @@ +SHELL := bash +NAME := auth-api + +ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI +include ../../.bingo/Variables.mk +endif + +include ../../.make/default.mk +include ../../.make/go.mk +include ../../.make/release.mk +include ../../.make/docs.mk diff --git a/services/auth-api/cmd/auth-api/main.go b/services/auth-api/cmd/auth-api/main.go new file mode 100644 index 0000000000..77d591d25c --- /dev/null +++ b/services/auth-api/cmd/auth-api/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/command" + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config/defaults" +) + +func main() { + cfg := defaults.DefaultConfig() + cfg.Context, _ = signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP) + if err := command.Execute(cfg); err != nil { + os.Exit(1) + } +} diff --git a/services/auth-api/pkg/command/root.go b/services/auth-api/pkg/command/root.go new file mode 100644 index 0000000000..1053eace93 --- /dev/null +++ b/services/auth-api/pkg/command/root.go @@ -0,0 +1,27 @@ +package command + +import ( + "os" + + "github.com/opencloud-eu/opencloud/pkg/clihelper" + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config" + "github.com/urfave/cli/v2" +) + +// GetCommands provides all commands for this service +func GetCommands(cfg *config.Config) cli.Commands { + return []*cli.Command{ + Server(cfg), + Version(cfg), + } +} + +func Execute(cfg *config.Config) error { + app := clihelper.DefaultApp(&cli.App{ + Name: "auth-api", + Usage: "OpenCloud authentication API for external services", + Commands: GetCommands(cfg), + }) + + return app.RunContext(cfg.Context, os.Args) +} diff --git a/services/auth-api/pkg/command/server.go b/services/auth-api/pkg/command/server.go new file mode 100644 index 0000000000..52c2e20df7 --- /dev/null +++ b/services/auth-api/pkg/command/server.go @@ -0,0 +1,98 @@ +package command + +import ( + "context" + "fmt" + + "github.com/oklog/run" + "github.com/opencloud-eu/opencloud/pkg/config/configlog" + "github.com/opencloud-eu/opencloud/pkg/tracing" + "github.com/opencloud-eu/opencloud/pkg/version" + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config/parser" + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/logging" + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/metrics" + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/server/debug" + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/server/http" + "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(_ *cli.Context) error { + return configlog.ReturnFatal(parser.ParseConfig(cfg)) + }, + Action: func(c *cli.Context) error { + logger := logging.Configure(cfg.Service.Name, cfg.Log) + + traceProvider, err := tracing.GetServiceTraceProvider(cfg.Tracing, cfg.Service.Name) + if err != nil { + return err + } + + var ( + gr = run.Group{} + ctx, cancel = context.WithCancel(c.Context) + m = metrics.New() + ) + + defer cancel() + + m.BuildInfo.WithLabelValues(version.GetString()).Set(1) + + server, err := debug.Server( + debug.Logger(logger), + debug.Config(cfg), + debug.Context(ctx), + ) + if err != nil { + logger.Info().Err(err).Str("transport", "debug").Msg("Failed to initialize server") + return err + } + + gr.Add(server.ListenAndServe, func(_ error) { + _ = server.Shutdown(ctx) + cancel() + }) + + httpServer, err := http.Server( + http.Logger(logger), + http.Context(ctx), + http.Config(cfg), + http.Metrics(m), + http.Namespace(cfg.HTTP.Namespace), + http.TraceProvider(traceProvider), + ) + if err != nil { + logger.Info(). + Err(err). + Str("transport", "http"). + Msg("Failed to initialize server") + + return err + } + + gr.Add(httpServer.Run, func(_ error) { + if err == nil { + logger.Info(). + Str("transport", "http"). + Str("server", cfg.Service.Name). + Msg("Shutting down server") + } else { + logger.Error().Err(err). + Str("transport", "http"). + Str("server", cfg.Service.Name). + Msg("Shutting down server") + } + + cancel() + }) + + return gr.Run() + }, + } +} diff --git a/services/auth-api/pkg/command/version.go b/services/auth-api/pkg/command/version.go new file mode 100644 index 0000000000..35b1243402 --- /dev/null +++ b/services/auth-api/pkg/command/version.go @@ -0,0 +1,26 @@ +package command + +import ( + "fmt" + + "github.com/opencloud-eu/opencloud/pkg/version" + + "github.com/opencloud-eu/opencloud/services/auth-api/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("") + + return nil + }, + } +} diff --git a/services/auth-api/pkg/config/config.go b/services/auth-api/pkg/config/config.go new file mode 100644 index 0000000000..68519b9ce1 --- /dev/null +++ b/services/auth-api/pkg/config/config.go @@ -0,0 +1,27 @@ +package config + +import ( + "context" + + "github.com/opencloud-eu/opencloud/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"` + + Authentication AuthenticationAPI `yaml:"authentication_api"` + + Context context.Context `yaml:"-"` +} + +type AuthenticationAPI struct { +} diff --git a/services/auth-api/pkg/config/debug.go b/services/auth-api/pkg/config/debug.go new file mode 100644 index 0000000000..c342ca0bd4 --- /dev/null +++ b/services/auth-api/pkg/config/debug.go @@ -0,0 +1,9 @@ +package config + +// Debug defines the available debug configuration. +type Debug struct { + Addr string `yaml:"addr" env:"AUTHAPI_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"1.0.0"` + Token string `yaml:"token" env:"AUTHAPI_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"1.0.0"` + Pprof bool `yaml:"pprof" env:"AUTHAPI_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"1.0.0"` + Zpages bool `yaml:"zpages" env:"AUTHAPI_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"1.0.0"` +} diff --git a/services/auth-api/pkg/config/defaults/defaultconfig.go b/services/auth-api/pkg/config/defaults/defaultconfig.go new file mode 100644 index 0000000000..5ea4748135 --- /dev/null +++ b/services/auth-api/pkg/config/defaults/defaultconfig.go @@ -0,0 +1,72 @@ +package defaults + +import ( + "strings" + + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config" +) + +// FullDefaultConfig returns a fully initialized default configuration +func FullDefaultConfig() *config.Config { + cfg := DefaultConfig() + EnsureDefaults(cfg) + Sanitize(cfg) + return cfg +} + +// DefaultConfig returns a basic default configuration +func DefaultConfig() *config.Config { + return &config.Config{ + Debug: config.Debug{ + Addr: "127.0.0.1:9202", + Token: "", + Pprof: false, + Zpages: false, + }, + HTTP: config.HTTP{ + Addr: "127.0.0.1:9278", + Root: "/auth", + Namespace: "eu.opencloud.web", + }, + Service: config.Service{ + Name: "auth-api", + }, + } +} + +// EnsureDefaults adds default values to the configuration if they are not set yet +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 + } +} + +// Sanitize sanitized the configuration +func Sanitize(cfg *config.Config) { + if cfg.HTTP.Root != "/" { + cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/") + } +} diff --git a/services/auth-api/pkg/config/http.go b/services/auth-api/pkg/config/http.go new file mode 100644 index 0000000000..61667b34eb --- /dev/null +++ b/services/auth-api/pkg/config/http.go @@ -0,0 +1,11 @@ +package config + +import "github.com/opencloud-eu/opencloud/pkg/shared" + +// HTTP defines the available http configuration. +type HTTP struct { + Addr string `yaml:"addr" env:"AUTHAPI_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"1.0.0"` + TLS shared.HTTPServiceTLS `yaml:"tls"` + Root string `yaml:"root" env:"AUTHAPI_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"1.0.0"` + Namespace string `yaml:"-"` +} diff --git a/services/auth-api/pkg/config/log.go b/services/auth-api/pkg/config/log.go new file mode 100644 index 0000000000..986d323bbd --- /dev/null +++ b/services/auth-api/pkg/config/log.go @@ -0,0 +1,9 @@ +package config + +// Log defines the available log configuration. +type Log struct { + Level string `mapstructure:"level" env:"OC_LOG_LEVEL;AUTHAPI_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"1.0.0"` + Pretty bool `mapstructure:"pretty" env:"OC_LOG_PRETTY;AUTHAPI_LOG_PRETTY" desc:"Activates pretty log output." introductionVersion:"1.0.0"` + Color bool `mapstructure:"color" env:"OC_LOG_COLOR;AUTHAPI_LOG_COLOR" desc:"Activates colorized log output." introductionVersion:"1.0.0"` + File string `mapstructure:"file" env:"OC_LOG_FILE;AUTHAPI_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set." introductionVersion:"1.0.0"` +} diff --git a/services/auth-api/pkg/config/parser/parse.go b/services/auth-api/pkg/config/parser/parse.go new file mode 100644 index 0000000000..8dbefde027 --- /dev/null +++ b/services/auth-api/pkg/config/parser/parse.go @@ -0,0 +1,39 @@ +package parser + +import ( + "errors" + + occfg "github.com/opencloud-eu/opencloud/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config/defaults" + + "github.com/opencloud-eu/opencloud/pkg/config/envdecode" +) + +// ParseConfig loads configuration from known paths. +func ParseConfig(cfg *config.Config) error { + err := occfg.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 + } + } + + // sanitize config + defaults.Sanitize(cfg) + + return Validate(cfg) +} + +// Validate can validate the configuration +func Validate(_ *config.Config) error { + return nil +} diff --git a/services/auth-api/pkg/config/service.go b/services/auth-api/pkg/config/service.go new file mode 100644 index 0000000000..d1eac383f0 --- /dev/null +++ b/services/auth-api/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/auth-api/pkg/config/tracing.go b/services/auth-api/pkg/config/tracing.go new file mode 100644 index 0000000000..c3e5e75a55 --- /dev/null +++ b/services/auth-api/pkg/config/tracing.go @@ -0,0 +1,21 @@ +package config + +import "github.com/opencloud-eu/opencloud/pkg/tracing" + +// Tracing defines the available tracing configuration. +type Tracing struct { + Enabled bool `yaml:"enabled" env:"OC_TRACING_ENABLED;AUTHAPI_TRACING_ENABLED" desc:"Activates tracing." introductionVersion:"1.0.0"` + Type string `yaml:"type" env:"OC_TRACING_TYPE;AUTHAPI_TRACING_TYPE" desc:"The type of tracing. Defaults to '', which is the same as 'jaeger'. Allowed tracing types are 'jaeger' and '' as of now." introductionVersion:"1.0.0"` + Endpoint string `yaml:"endpoint" env:"OC_TRACING_ENDPOINT;AUTHAPI_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent." introductionVersion:"1.0.0"` + Collector string `yaml:"collector" env:"OC_TRACING_COLLECTOR;AUTHAPI_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." introductionVersion:"1.0.0"` +} + +// Convert Tracing to the tracing package's Config struct. +func (t Tracing) Convert() tracing.Config { + return tracing.Config{ + Enabled: t.Enabled, + Type: t.Type, + Endpoint: t.Endpoint, + Collector: t.Collector, + } +} diff --git a/services/auth-api/pkg/logging/logging.go b/services/auth-api/pkg/logging/logging.go new file mode 100644 index 0000000000..b63a748887 --- /dev/null +++ b/services/auth-api/pkg/logging/logging.go @@ -0,0 +1,17 @@ +package logging + +import ( + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config" +) + +// Configure 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/auth-api/pkg/metrics/metrics.go b/services/auth-api/pkg/metrics/metrics.go new file mode 100644 index 0000000000..7f2c828660 --- /dev/null +++ b/services/auth-api/pkg/metrics/metrics.go @@ -0,0 +1,34 @@ +package metrics + +import "github.com/prometheus/client_golang/prometheus" + +var ( + // Namespace defines the namespace for the defines metrics. + Namespace = "opencloud" + + // Subsystem defines the subsystem for the defines metrics. + Subsystem = "authentication-api" +) + +// Metrics defines the available metrics of this service. +type Metrics struct { + BuildInfo *prometheus.GaugeVec +} + +// New initializes the available metrics. +func New() *Metrics { + m := &Metrics{ + BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: Namespace, + Subsystem: Subsystem, + Name: "build_info", + Help: "Build information", + }, []string{"version"}), + } + + _ = prometheus.Register( + m.BuildInfo, + ) + + return m +} diff --git a/services/auth-api/pkg/server/debug/option.go b/services/auth-api/pkg/server/debug/option.go new file mode 100644 index 0000000000..59e0d488d1 --- /dev/null +++ b/services/auth-api/pkg/server/debug/option.go @@ -0,0 +1,50 @@ +package debug + +import ( + "context" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/auth-api/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/auth-api/pkg/server/debug/server.go b/services/auth-api/pkg/server/debug/server.go new file mode 100644 index 0000000000..3f54c66012 --- /dev/null +++ b/services/auth-api/pkg/server/debug/server.go @@ -0,0 +1,24 @@ +package debug + +import ( + "net/http" + + "github.com/opencloud-eu/opencloud/pkg/handlers" + "github.com/opencloud-eu/opencloud/pkg/service/debug" + "github.com/opencloud-eu/opencloud/pkg/version" +) + +// Server initializes the debug service and server. +func Server(opts ...Option) (*http.Server, error) { + options := newOptions(opts...) + + readyHandlerConfiguration := handlers.NewCheckHandlerConfiguration(). + WithLogger(options.Logger) + + return debug.NewService( + debug.Logger(options.Logger), + debug.Name(options.Config.Service.Name), + debug.Version(version.GetString()), + debug.Ready(handlers.NewCheckHandler(readyHandlerConfiguration)), + ), nil +} diff --git a/services/auth-api/pkg/server/http/option.go b/services/auth-api/pkg/server/http/option.go new file mode 100644 index 0000000000..cb4b0b5f75 --- /dev/null +++ b/services/auth-api/pkg/server/http/option.go @@ -0,0 +1,83 @@ +package http + +import ( + "context" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/metrics" + "github.com/urfave/cli/v2" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" +) + +// Option defines a single option function. +type Option func(o *Options) + +// Options defines the available options for this package. +type Options struct { + Namespace string + Logger log.Logger + Context context.Context + Config *config.Config + Metrics *metrics.Metrics + Flags []cli.Flag + TraceProvider trace.TracerProvider +} + +// 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 + } +} + +// Metrics provides a function to set the metrics option. +func Metrics(val *metrics.Metrics) Option { + return func(o *Options) { + o.Metrics = val + } +} + +// Namespace provides a function to set the Namespace option. +func Namespace(val string) Option { + return func(o *Options) { + o.Namespace = val + } +} + +// TraceProvider provides a function to configure the trace provider +func TraceProvider(traceProvider trace.TracerProvider) Option { + return func(o *Options) { + if traceProvider != nil { + o.TraceProvider = traceProvider + } else { + o.TraceProvider = noop.NewTracerProvider() + } + } +} diff --git a/services/auth-api/pkg/server/http/server.go b/services/auth-api/pkg/server/http/server.go new file mode 100644 index 0000000000..0237b55f55 --- /dev/null +++ b/services/auth-api/pkg/server/http/server.go @@ -0,0 +1,61 @@ +package http + +import ( + "fmt" + + "github.com/go-chi/chi/v5/middleware" + opencloudmiddleware "github.com/opencloud-eu/opencloud/pkg/middleware" + "github.com/opencloud-eu/opencloud/pkg/service/http" + "github.com/opencloud-eu/opencloud/pkg/version" + svc "github.com/opencloud-eu/opencloud/services/auth-api/pkg/service/http/v0" + "go-micro.dev/v4" +) + +// Server initializes the http service and server. +func Server(opts ...Option) (http.Service, error) { + options := newOptions(opts...) + + fmt.Printf("===== HTTP addr: %v\n", options.Config.HTTP.Addr) + + service, err := http.NewService( + http.TLSConfig(options.Config.HTTP.TLS), + http.Logger(options.Logger), + http.Name(options.Config.Service.Name), + http.Version(version.GetString()), + http.Namespace(options.Config.HTTP.Namespace), + http.Address(options.Config.HTTP.Addr), + http.Context(options.Context), + http.TraceProvider(options.TraceProvider), + ) + if err != nil { + options.Logger.Error(). + Err(err). + Msg("Error initializing http service") + return http.Service{}, fmt.Errorf("could not initialize http service: %w", err) + } + + handle := svc.NewService( + svc.Logger(options.Logger), + svc.Config(options.Config), + svc.Middleware( + middleware.RealIP, + middleware.RequestID, + opencloudmiddleware.Version( + options.Config.Service.Name, + version.GetString(), + ), + opencloudmiddleware.Logger(options.Logger), + ), + ) + + { + handle = svc.NewInstrument(handle, options.Metrics) + handle = svc.NewLogging(handle, options.Logger) + } + + if err := micro.RegisterHandler(service.Server(), handle); err != nil { + return http.Service{}, err + } + + return service, nil +} diff --git a/services/auth-api/pkg/service/http/v0/instrument.go b/services/auth-api/pkg/service/http/v0/instrument.go new file mode 100644 index 0000000000..d1a6663ecb --- /dev/null +++ b/services/auth-api/pkg/service/http/v0/instrument.go @@ -0,0 +1,25 @@ +package svc + +import ( + "net/http" + + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/metrics" +) + +// 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 +} + +// ServeHTTP implements the Service interface. +func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) { + i.next.ServeHTTP(w, r) +} diff --git a/services/auth-api/pkg/service/http/v0/logging.go b/services/auth-api/pkg/service/http/v0/logging.go new file mode 100644 index 0000000000..c21734ce11 --- /dev/null +++ b/services/auth-api/pkg/service/http/v0/logging.go @@ -0,0 +1,25 @@ +package svc + +import ( + "net/http" + + "github.com/opencloud-eu/opencloud/pkg/log" +) + +// 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 +} + +// ServeHTTP implements the Service interface. +func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) { + l.next.ServeHTTP(w, r) +} diff --git a/services/auth-api/pkg/service/http/v0/option.go b/services/auth-api/pkg/service/http/v0/option.go new file mode 100644 index 0000000000..581a7b6631 --- /dev/null +++ b/services/auth-api/pkg/service/http/v0/option.go @@ -0,0 +1,52 @@ +package svc + +import ( + "net/http" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config" + "go.opentelemetry.io/otel/trace" +) + +// 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 + Middleware []func(http.Handler) http.Handler + TraceProvider trace.TracerProvider +} + +// 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 + } +} + +// Middleware provides a function to set the middleware option. +func Middleware(val ...func(http.Handler) http.Handler) Option { + return func(o *Options) { + o.Middleware = val + } +} diff --git a/services/auth-api/pkg/service/http/v0/service.go b/services/auth-api/pkg/service/http/v0/service.go new file mode 100644 index 0000000000..a779d33a2c --- /dev/null +++ b/services/auth-api/pkg/service/http/v0/service.go @@ -0,0 +1,135 @@ +package svc + +import ( + "net/http" + "regexp" + "time" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/golang-jwt/jwt/v5" + "github.com/riandyrn/otelchi" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/pkg/tracing" + "github.com/opencloud-eu/opencloud/services/auth-api/pkg/config" +) + +// Service defines the service handlers. +type Service interface { + ServeHTTP(w http.ResponseWriter, r *http.Request) +} + +// NewService returns a service implementation for Service. +func NewService(opts ...Option) Service { + options := newOptions(opts...) + + m := chi.NewMux() + m.Use(options.Middleware...) + + m.Use( + otelchi.Middleware( + "auth-api", + otelchi.WithChiRoutes(m), + otelchi.WithTracerProvider(options.TraceProvider), + otelchi.WithPropagators(tracing.GetPropagator()), + ), + ) + + svc := NewAuthenticationApi(options.Config, &options.Logger, m) + + m.Route(options.Config.HTTP.Root, func(r chi.Router) { + r.Get("/", svc.Authenticate) + r.Post("/", svc.Authenticate) + }) + + _ = chi.Walk(m, func(method string, route string, _ http.Handler, middlewares ...func(http.Handler) http.Handler) error { + options.Logger.Debug().Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint") + return nil + }) + + return svc +} + +type AuthenticationApi struct { + config *config.Config + logger *log.Logger + mux *chi.Mux +} + +func NewAuthenticationApi(config *config.Config, logger *log.Logger, mux *chi.Mux) *AuthenticationApi { + return &AuthenticationApi{ + config: config, + mux: mux, + logger: logger, + } +} + +func (a AuthenticationApi) ServeHTTP(w http.ResponseWriter, r *http.Request) { + a.mux.ServeHTTP(w, r) +} + +type AuthResponse struct { + Subject string +} + +func (AuthResponse) Render(w http.ResponseWriter, r *http.Request) error { + return nil +} + +var authRegex = regexp.MustCompile("^(i:Basic|Bearer)\\s+(.+)$") + +func (a AuthenticationApi) Authenticate(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth == "" { + w.WriteHeader(http.StatusBadRequest) // authentication header is missing altogether + return + } + matches := authRegex.FindAllString(auth, 2) + if matches == nil { + w.WriteHeader(http.StatusBadRequest) // authentication header is unsupported + return + } + + if matches[0] == "Basic" { + username, password, ok := r.BasicAuth() + if !ok { + w.WriteHeader(http.StatusBadRequest) // failed to decode the basic credentials + } + if password == "secret" { + _ = render.Render(w, r, AuthResponse{Subject: username}) + } else { + w.WriteHeader(http.StatusUnauthorized) + } + } else if matches[0] == "Bearer" { + claims := jwt.MapClaims{} + publicKey := nil + tokenString := matches[1] + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + token.Header["kid"] + return publicKey, nil + }, jwt.WithExpirationRequired(), jwt.WithLeeway(5*time.Second)) + if err != nil { + w.WriteHeader(http.StatusBadRequest) // failed to parse bearer token + } + sub, err := token.Claims.GetSubject() + if err != nil { + w.WriteHeader(http.StatusBadRequest) // failed to extract sub claim from bearer token + } + _ = render.Render(w, r, AuthResponse{Subject: sub}) + } else { + w.WriteHeader(http.StatusBadRequest) // authentication header is unsupported + return + } + + // TODO + + /* + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + */ + + _ = render.Render(w, r, AuthResponse{Subject: "todo"}) +} diff --git a/services/groupware/Makefile b/services/groupware/Makefile index b780ba9e92..24a42cc210 100644 --- a/services/groupware/Makefile +++ b/services/groupware/Makefile @@ -6,11 +6,6 @@ include ../../.bingo/Variables.mk endif include ../../.make/default.mk -include ../../.make/recursion.mk include ../../.make/go.mk include ../../.make/release.mk include ../../.make/docs.mk - -.PHONY: go-generate -go-generate: $(MOCKERY) - $(MOCKERY) diff --git a/services/groupware/pkg/config/log.go b/services/groupware/pkg/config/log.go index 4e3cea917f..c9464300ce 100644 --- a/services/groupware/pkg/config/log.go +++ b/services/groupware/pkg/config/log.go @@ -2,8 +2,8 @@ package config // Log defines the available log configuration. type Log struct { - Level string `mapstructure:"level" env:"OC_LOG_LEVEL;THUMBNAILS_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"1.0.0"` - Pretty bool `mapstructure:"pretty" env:"OC_LOG_PRETTY;THUMBNAILS_LOG_PRETTY" desc:"Activates pretty log output." introductionVersion:"1.0.0"` - Color bool `mapstructure:"color" env:"OC_LOG_COLOR;THUMBNAILS_LOG_COLOR" desc:"Activates colorized log output." introductionVersion:"1.0.0"` - File string `mapstructure:"file" env:"OC_LOG_FILE;THUMBNAILS_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set." introductionVersion:"1.0.0"` + Level string `mapstructure:"level" env:"OC_LOG_LEVEL;GROUPWARE_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"1.0.0"` + Pretty bool `mapstructure:"pretty" env:"OC_LOG_PRETTY;GROUPWARE_LOG_PRETTY" desc:"Activates pretty log output." introductionVersion:"1.0.0"` + Color bool `mapstructure:"color" env:"OC_LOG_COLOR;GROUPWARE_LOG_COLOR" desc:"Activates colorized log output." introductionVersion:"1.0.0"` + File string `mapstructure:"file" env:"OC_LOG_FILE;GROUPWARE_LOG_FILE" desc:"The path to the log file. Activates logging to this file if set." introductionVersion:"1.0.0"` } diff --git a/services/groupware/pkg/config/tracing.go b/services/groupware/pkg/config/tracing.go index 375b4024db..0ef623cee1 100644 --- a/services/groupware/pkg/config/tracing.go +++ b/services/groupware/pkg/config/tracing.go @@ -4,10 +4,10 @@ import "github.com/opencloud-eu/opencloud/pkg/tracing" // Tracing defines the available tracing configuration. type Tracing struct { - Enabled bool `yaml:"enabled" env:"OC_TRACING_ENABLED;ANTIVIRUS_TRACING_ENABLED" desc:"Activates tracing." introductionVersion:"1.0.0"` - Type string `yaml:"type" env:"OC_TRACING_TYPE;ANTIVIRUS_TRACING_TYPE" desc:"The type of tracing. Defaults to '', which is the same as 'jaeger'. Allowed tracing types are 'jaeger' and '' as of now." introductionVersion:"1.0.0"` - Endpoint string `yaml:"endpoint" env:"OC_TRACING_ENDPOINT;ANTIVIRUS_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent." introductionVersion:"1.0.0"` - Collector string `yaml:"collector" env:"OC_TRACING_COLLECTOR;ANTIVIRUS_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." introductionVersion:"1.0.0"` + Enabled bool `yaml:"enabled" env:"OC_TRACING_ENABLED;GROUPWARE_TRACING_ENABLED" desc:"Activates tracing." introductionVersion:"1.0.0"` + Type string `yaml:"type" env:"OC_TRACING_TYPE;GROUPWARE_TRACING_TYPE" desc:"The type of tracing. Defaults to '', which is the same as 'jaeger'. Allowed tracing types are 'jaeger' and '' as of now." introductionVersion:"1.0.0"` + Endpoint string `yaml:"endpoint" env:"OC_TRACING_ENDPOINT;GROUPWARE_TRACING_ENDPOINT" desc:"The endpoint of the tracing agent." introductionVersion:"1.0.0"` + Collector string `yaml:"collector" env:"OC_TRACING_COLLECTOR;GROUPWARE_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." introductionVersion:"1.0.0"` } // Convert Tracing to the tracing package's Config struct. diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index e3b9893abc..db8e72df5f 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -292,8 +292,13 @@ func DefaultPolicies() []config.Policy { SkipXAccessToken: true, }, { - Endpoint: "/groupware/", - Service: "eu.opencloud.web.groupware", + Endpoint: "/groupware", + Service: "eu.opencloud.web.groupware", + Unprotected: true, + }, + { + Endpoint: "/auth", + Service: "eu.opencloud.web.auth-api", Unprotected: true, }, },