add an auth-api service to make an exemplary implementation of an external authentication API for third party services such as Stalwart

This commit is contained in:
Pascal Bleser
2025-05-07 09:26:28 +02:00
parent 4cf4d44321
commit 4e6053cdbd
32 changed files with 918 additions and 20 deletions

View File

@@ -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 \

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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(),

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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()
},
}
}

View File

@@ -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
},
}
}

View File

@@ -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 {
}

View File

@@ -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"`
}

View File

@@ -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, "/")
}
}

View File

@@ -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:"-"`
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -0,0 +1,6 @@
package config
// Service defines the available service configuration.
type Service struct {
Name string `yaml:"-"`
}

View File

@@ -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,
}
}

View File

@@ -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),
)
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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()
}
}
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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"})
}

View File

@@ -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)

View File

@@ -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"`
}

View File

@@ -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.

View File

@@ -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,
},
},