From 19fc7dfb8b23a6a71920741a29d3dd22316199d4 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Tue, 22 Sep 2026 14:38:30 +0200 Subject: [PATCH 01/32] feat(guestauth): added guestauth service skeleton --- services/guestauth/Makefile | 11 ++ services/guestauth/README.md | 17 +++ services/guestauth/pkg/command/health.go | 18 +++ services/guestauth/pkg/command/root.go | 34 ++++++ services/guestauth/pkg/command/server.go | 104 ++++++++++++++++++ services/guestauth/pkg/command/version.go | 18 +++ services/guestauth/pkg/config/config.go | 71 ++++++++++++ services/guestauth/pkg/config/debug.go | 9 ++ .../pkg/config/defaults/defaultconfig.go | 74 +++++++++++++ services/guestauth/pkg/config/parser/parse.go | 38 +++++++ services/guestauth/pkg/config/service.go | 6 + services/guestauth/pkg/metrics/metrics.go | 35 ++++++ services/guestauth/pkg/server/debug/option.go | 50 +++++++++ services/guestauth/pkg/server/debug/server.go | 40 +++++++ services/guestauth/pkg/server/http/option.go | 59 ++++++++++ services/guestauth/pkg/server/http/server.go | 69 ++++++++++++ services/guestauth/pkg/service/options.go | 39 +++++++ services/guestauth/pkg/service/service.go | 52 +++++++++ 18 files changed, 744 insertions(+) create mode 100644 services/guestauth/Makefile create mode 100644 services/guestauth/README.md create mode 100644 services/guestauth/pkg/command/health.go create mode 100644 services/guestauth/pkg/command/root.go create mode 100644 services/guestauth/pkg/command/server.go create mode 100644 services/guestauth/pkg/command/version.go create mode 100644 services/guestauth/pkg/config/config.go create mode 100644 services/guestauth/pkg/config/debug.go create mode 100644 services/guestauth/pkg/config/defaults/defaultconfig.go create mode 100644 services/guestauth/pkg/config/parser/parse.go create mode 100644 services/guestauth/pkg/config/service.go create mode 100644 services/guestauth/pkg/metrics/metrics.go create mode 100644 services/guestauth/pkg/server/debug/option.go create mode 100644 services/guestauth/pkg/server/debug/server.go create mode 100644 services/guestauth/pkg/server/http/option.go create mode 100644 services/guestauth/pkg/server/http/server.go create mode 100644 services/guestauth/pkg/service/options.go create mode 100644 services/guestauth/pkg/service/service.go diff --git a/services/guestauth/Makefile b/services/guestauth/Makefile new file mode 100644 index 0000000000..4cae818a68 --- /dev/null +++ b/services/guestauth/Makefile @@ -0,0 +1,11 @@ +SHELL := bash +NAME := guestauth + +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 \ No newline at end of file diff --git a/services/guestauth/README.md b/services/guestauth/README.md new file mode 100644 index 0000000000..52909479e1 --- /dev/null +++ b/services/guestauth/README.md @@ -0,0 +1,17 @@ +# Guestauth + +The `guestauth` service is responsible for creating and validating guest login tokens and exchanging them for a session cookie. + +It is part of the default service set. It does not need to be explicitly enabled with `OC_ADD_RUN_SERVICES`. + +## Token lifecycle + +- When a guest share is created (`ShareCreated` event), the service generates a secure random token, persists it and emits a `GuestTokenCreated` event. +- When a guest share is removed or expires (`ShareRemoved` / `ShareExpired` events),the service cleans up the stored token. +- The token can be redeemed via `POST /graph/v1beta1/guestInvitations/redeem` to exchange it for a session cookie. + +## Configuration + +The service can be configured via environment variables (prefix `GUESTAUTH_*`)or a `guestauth.yaml` configuration file. + +To run the service without consuming events, set `GUESTAUTH_EVENTS_DISABLED=true`. To run it without the HTTP service, set `GUESTAUTH_HTTP_DISABLED=true`. \ No newline at end of file diff --git a/services/guestauth/pkg/command/health.go b/services/guestauth/pkg/command/health.go new file mode 100644 index 0000000000..31dfe507ad --- /dev/null +++ b/services/guestauth/pkg/command/health.go @@ -0,0 +1,18 @@ +package command + +import ( + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" + "github.com/spf13/cobra" +) + +// Health is the entrypoint for the health command. +func Health(cfg *config.Config) *cobra.Command { + return &cobra.Command{ + Use: "health", + Short: "Check health status", + RunE: func(cmd *cobra.Command, args []string) error { + // not implemented + return nil + }, + } +} diff --git a/services/guestauth/pkg/command/root.go b/services/guestauth/pkg/command/root.go new file mode 100644 index 0000000000..775abdf7a0 --- /dev/null +++ b/services/guestauth/pkg/command/root.go @@ -0,0 +1,34 @@ +package command + +import ( + "os" + + "github.com/opencloud-eu/opencloud/pkg/clihelper" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" + "github.com/spf13/cobra" +) + +// GetCommands provides all commands for this service +func GetCommands(cfg *config.Config) []*cobra.Command { + return []*cobra.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 guestauth command. +func Execute(cfg *config.Config) error { + app := clihelper.DefaultApp(&cobra.Command{ + Use: "guestauth", + Short: "starts guestauth service", + }) + app.AddCommand(GetCommands(cfg)...) + app.SetArgs(os.Args[1:]) + return app.ExecuteContext(cfg.Context) +} diff --git a/services/guestauth/pkg/command/server.go b/services/guestauth/pkg/command/server.go new file mode 100644 index 0000000000..e918e46ea7 --- /dev/null +++ b/services/guestauth/pkg/command/server.go @@ -0,0 +1,104 @@ +package command + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/opencloud-eu/opencloud/pkg/config/configlog" + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/pkg/runner" + "github.com/opencloud-eu/opencloud/pkg/version" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config/parser" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/metrics" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/server/debug" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/server/http" + svc "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service" +) + +// Server is the entrypoint for the server command. +func Server(cfg *config.Config) *cobra.Command { + return &cobra.Command{ + Use: "server", + Short: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name), + PreRunE: func(cmd *cobra.Command, args []string) error { + return configlog.ReturnFatal(parser.ParseConfig(cfg)) + }, + RunE: func(cmd *cobra.Command, args []string) error { + logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel) + + gr := runner.NewGroup() + ctx, cancel := context.WithCancel(cmd.Context()) + defer cancel() + + mtrcs := metrics.New() + mtrcs.BuildInfo.WithLabelValues(version.GetString()).Set(1) + + if !cfg.HTTP.Disabled { + server, err := http.Server( + http.Logger(logger), + http.Context(ctx), + http.Config(cfg), + ) + if err != nil { + logger.Info(). + Err(err). + Str("transport", "http"). + Msg("Failed to initialize server") + + return err + } + + gr.Add(runner.NewGoMicroHttpServerRunner(cfg.Service.Name+".http", server)) + } else { + logger.Info().Msg("HTTP server disabled, not starting HTTP service") + } + + if !cfg.Events.Disabled { + guestAuth, err := svc.New( + svc.Logger(logger), + svc.Context(ctx), + svc.Config(cfg), + ) + if err != nil { + logger.Error().Err(err).Str("transport", "event").Msg("Failed to initialize server") + return err + } + + gr.Add(runner.New(cfg.Service.Name+".svc", func() error { + return guestAuth.Run() + }, func() { + guestAuth.Close() + })) + } else { + logger.Info().Msg("event listening disabled, not starting event service") + } + + { + debugServer, err := debug.Server( + debug.Logger(logger), + debug.Context(ctx), + debug.Config(cfg), + ) + if err != nil { + logger.Info().Err(err).Str("server", "debug").Msg("Failed to initialize server") + return err + } + + gr.Add(runner.NewGolangHttpServerRunner(cfg.Service.Name+".debug", debugServer)) + } + + grResults := gr.Run(ctx) + + // return the first non-nil error found in the results + for _, grResult := range grResults { + if grResult.RunnerError != nil { + return grResult.RunnerError + } + } + return nil + }, + } +} diff --git a/services/guestauth/pkg/command/version.go b/services/guestauth/pkg/command/version.go new file mode 100644 index 0000000000..1debe78edb --- /dev/null +++ b/services/guestauth/pkg/command/version.go @@ -0,0 +1,18 @@ +package command + +import ( + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" + "github.com/spf13/cobra" +) + +// Version prints the service versions of all running instances. +func Version(cfg *config.Config) *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print the version of this binary and the running service instances", + RunE: func(cmd *cobra.Command, args []string) error { + // not implemented + return nil + }, + } +} diff --git a/services/guestauth/pkg/config/config.go b/services/guestauth/pkg/config/config.go new file mode 100644 index 0000000000..a9b055102e --- /dev/null +++ b/services/guestauth/pkg/config/config.go @@ -0,0 +1,71 @@ +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:"-"` + + LogLevel string `yaml:"loglevel" env:"OC_LOG_LEVEL;GUESTAUTH_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"1.0.0"` + + Debug Debug `yaml:"debug"` + + Events Events `yaml:"events"` + + RevaGateway string `yaml:"reva_gateway" env:"OC_REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata" introductionVersion:"1.0.0"` + GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"` + + HTTP HTTP `yaml:"http"` + TokenManager *TokenManager `yaml:"token_manager"` + + ServiceAccount ServiceAccount `yaml:"service_account"` + + Context context.Context `yaml:"-"` +} + +// Events combines the configuration options for the event bus. +type Events struct { + Disabled bool `yaml:"disabled" env:"GUESTAUTH_EVENTS_DISABLED" desc:"Disables listening for events. Set this to true if the service should only handle HTTP requests." introductionVersion:"1.0.0"` + Endpoint string `yaml:"endpoint" env:"OC_EVENTS_ENDPOINT" desc:"The address of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture." introductionVersion:"1.0.0"` + Cluster string `yaml:"cluster" env:"OC_EVENTS_CLUSTER" desc:"The clusterID of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture. Mandatory when using NATS as event system." introductionVersion:"1.0.0"` + TLSInsecure bool `yaml:"tls_insecure" env:"OC_INSECURE;OC_EVENTS_TLS_INSECURE" desc:"Whether to verify the server TLS certificates." introductionVersion:"1.0.0"` + TLSRootCACertificate string `yaml:"tls_root_ca_certificate" env:"OC_EVENTS_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided GUESTAUTH_EVENTS_TLS_INSECURE will be seen as false." introductionVersion:"1.0.0"` + EnableTLS bool `yaml:"enable_tls" env:"OC_EVENTS_ENABLE_TLS" desc:"Enable TLS for the connection to the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"1.0.0"` + AuthUsername string `yaml:"username" env:"OC_EVENTS_AUTH_USERNAME" desc:"The username to authenticate with the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"1.0.0"` + AuthPassword string `yaml:"password" env:"OC_EVENTS_AUTH_PASSWORD" desc:"The password to authenticate with the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"1.0.0"` +} + +// ServiceAccount is the configuration for the used service account +type ServiceAccount struct { + ServiceAccountID string `yaml:"service_account_id" env:"OC_SERVICE_ACCOUNT_ID;GUESTAUTH_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details." introductionVersion:"1.0.0"` + ServiceAccountSecret string `yaml:"service_account_secret" env:"OC_SERVICE_ACCOUNT_SECRET;GUESTAUTH_SERVICE_ACCOUNT_SECRET" desc:"The service account secret." introductionVersion:"1.0.0"` +} + +// CORS defines the available cors configuration. +type CORS struct { + AllowedOrigins []string `yaml:"allow_origins" env:"OC_CORS_ALLOW_ORIGINS;GUESTAUTH_CORS_ALLOW_ORIGINS" desc:"A 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. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"` + AllowedMethods []string `yaml:"allow_methods" env:"OC_CORS_ALLOW_METHODS;GUESTAUTH_CORS_ALLOW_METHODS" desc:"A 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. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"` + AllowedHeaders []string `yaml:"allow_headers" env:"OC_CORS_ALLOW_HEADERS;GUESTAUTH_CORS_ALLOW_HEADERS" desc:"A 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. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"` + AllowCredentials bool `yaml:"allow_credentials" env:"OC_CORS_ALLOW_CREDENTIALS;GUESTAUTH_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." introductionVersion:"1.0.0"` +} + +// HTTP defines the available http configuration. +type HTTP struct { + Disabled bool `yaml:"disabled" env:"GUESTAUTH_HTTP_DISABLED" desc:"Disables the HTTP service. Set this to true if the service should only handle events." introductionVersion:"1.0.0"` + Addr string `yaml:"addr" env:"GUESTAUTH_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"1.0.0"` + Namespace string `yaml:"-"` + Root string `yaml:"root" env:"GUESTAUTH_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"1.0.0"` + CORS CORS `yaml:"cors"` + TLS shared.HTTPServiceTLS `yaml:"tls"` +} + +// TokenManager is the config for using the reva token manager +type TokenManager struct { + JWTSecret string `yaml:"jwt_secret" env:"OC_JWT_SECRET;GUESTAUTH_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"1.0.0"` +} diff --git a/services/guestauth/pkg/config/debug.go b/services/guestauth/pkg/config/debug.go new file mode 100644 index 0000000000..e7da076c0e --- /dev/null +++ b/services/guestauth/pkg/config/debug.go @@ -0,0 +1,9 @@ +package config + +// Debug defines the available debug configuration. +type Debug struct { + Addr string `yaml:"addr" env:"GUESTAUTH_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:"GUESTAUTH_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"1.0.0"` + Pprof bool `yaml:"pprof" env:"GUESTAUTH_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"1.0.0"` + Zpages bool `yaml:"zpages" env:"GUESTAUTH_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"1.0.0"` +} diff --git a/services/guestauth/pkg/config/defaults/defaultconfig.go b/services/guestauth/pkg/config/defaults/defaultconfig.go new file mode 100644 index 0000000000..ae0b054002 --- /dev/null +++ b/services/guestauth/pkg/config/defaults/defaultconfig.go @@ -0,0 +1,74 @@ +package defaults + +import ( + "github.com/opencloud-eu/opencloud/pkg/shared" + "github.com/opencloud-eu/opencloud/pkg/structs" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" +) + +// FullDefaultConfig returns the full default config +func FullDefaultConfig() *config.Config { + cfg := DefaultConfig() + EnsureDefaults(cfg) + Sanitize(cfg) + return cfg +} + +// DefaultConfig return the default configuration +func DefaultConfig() *config.Config { + return &config.Config{ + Debug: config.Debug{ + Addr: "127.0.0.1:9267", + Token: "", + Pprof: false, + Zpages: false, + }, + Service: config.Service{ + Name: "guestauth", + }, + Events: config.Events{ + Endpoint: "127.0.0.1:9233", + Cluster: "opencloud-cluster", + EnableTLS: false, + }, + RevaGateway: shared.DefaultRevaConfig().Address, + HTTP: config.HTTP{ + Addr: "127.0.0.1:9266", + Root: "/graph", + Namespace: "eu.opencloud.web", + CORS: config.CORS{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, + AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With", "X-Request-Id", "Ocs-Apirequest"}, + AllowCredentials: true, + }, + }, + } +} + +// EnsureDefaults ensures the config contains default values +func EnsureDefaults(cfg *config.Config) { + if cfg.LogLevel == "" { + cfg.LogLevel = "error" + } + if cfg.GRPCClientTLS == nil && cfg.Commons != nil { + cfg.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS) + } + + if cfg.TokenManager == nil && cfg.Commons != nil && cfg.Commons.TokenManager != nil { + cfg.TokenManager = &config.TokenManager{ + JWTSecret: cfg.Commons.TokenManager.JWTSecret, + } + } else if cfg.TokenManager == nil { + cfg.TokenManager = &config.TokenManager{} + } + + if cfg.Commons != nil { + cfg.HTTP.TLS = cfg.Commons.HTTPServiceTLS + } +} + +// Sanitize sanitizes the config +func Sanitize(cfg *config.Config) { + // sanitize config +} diff --git a/services/guestauth/pkg/config/parser/parse.go b/services/guestauth/pkg/config/parser/parse.go new file mode 100644 index 0000000000..4702330dfc --- /dev/null +++ b/services/guestauth/pkg/config/parser/parse.go @@ -0,0 +1,38 @@ +package parser + +import ( + "errors" + + occfg "github.com/opencloud-eu/opencloud/pkg/config" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" + "github.com/opencloud-eu/opencloud/services/guestauth/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 + } + } + + defaults.Sanitize(cfg) + + return Validate(cfg) +} + +// Validate validates the config +func Validate(cfg *config.Config) error { + return nil +} diff --git a/services/guestauth/pkg/config/service.go b/services/guestauth/pkg/config/service.go new file mode 100644 index 0000000000..d1eac383f0 --- /dev/null +++ b/services/guestauth/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/guestauth/pkg/metrics/metrics.go b/services/guestauth/pkg/metrics/metrics.go new file mode 100644 index 0000000000..d4f167cc33 --- /dev/null +++ b/services/guestauth/pkg/metrics/metrics.go @@ -0,0 +1,35 @@ +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 = "guestauth" +) + +// 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, + ) + + // TODO: implement metrics + return m +} diff --git a/services/guestauth/pkg/server/debug/option.go b/services/guestauth/pkg/server/debug/option.go new file mode 100644 index 0000000000..3f11ec3e46 --- /dev/null +++ b/services/guestauth/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/guestauth/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/guestauth/pkg/server/debug/server.go b/services/guestauth/pkg/server/debug/server.go new file mode 100644 index 0000000000..79a016f421 --- /dev/null +++ b/services/guestauth/pkg/server/debug/server.go @@ -0,0 +1,40 @@ +package debug + +import ( + "net/http" + + "github.com/opencloud-eu/opencloud/pkg/checks" + "github.com/opencloud-eu/opencloud/pkg/handlers" + "github.com/opencloud-eu/opencloud/pkg/nats" + "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...) + + healthHandlerConfiguration := handlers.NewCheckHandlerConfiguration(). + WithLogger(options.Logger). + WithCheck("http reachability", checks.NewHTTPCheck(options.Config.HTTP.Addr)) + + secureOption := nats.Secure( + options.Config.Events.EnableTLS, + options.Config.Events.TLSInsecure, + options.Config.Events.TLSRootCACertificate, + ) + readyHandlerConfiguration := healthHandlerConfiguration. + WithCheck("nats reachability", checks.NewNatsCheck(options.Config.Events.Endpoint, secureOption)) + + 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(handlers.NewCheckHandler(healthHandlerConfiguration)), + debug.Ready(handlers.NewCheckHandler(readyHandlerConfiguration)), + ), nil +} diff --git a/services/guestauth/pkg/server/http/option.go b/services/guestauth/pkg/server/http/option.go new file mode 100644 index 0000000000..1dca47e974 --- /dev/null +++ b/services/guestauth/pkg/server/http/option.go @@ -0,0 +1,59 @@ +package http + +import ( + "context" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" + "github.com/spf13/pflag" +) + +// 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 + Flags []pflag.Flag +} + +// 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 + } +} + +// Flags provides a function to set the flags option. +func Flags(flags ...pflag.Flag) Option { + return func(o *Options) { + o.Flags = append(o.Flags, flags...) + } +} diff --git a/services/guestauth/pkg/server/http/server.go b/services/guestauth/pkg/server/http/server.go new file mode 100644 index 0000000000..8b0c953d6e --- /dev/null +++ b/services/guestauth/pkg/server/http/server.go @@ -0,0 +1,69 @@ +package http + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + chimiddleware "github.com/go-chi/chi/v5/middleware" + "github.com/opencloud-eu/opencloud/pkg/cors" + "github.com/opencloud-eu/opencloud/pkg/middleware" + ohttp "github.com/opencloud-eu/opencloud/pkg/service/http" + "github.com/opencloud-eu/opencloud/pkg/version" + "go-micro.dev/v4" +) + +// Server initializes the http service and server. +func Server(opts ...Option) (ohttp.Service, error) { + options := newOptions(opts...) + + newService, 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 + } + + middlewares := []func(http.Handler) http.Handler{ + chimiddleware.RequestID, + middleware.Version( + options.Config.Service.Name, + version.GetString(), + ), + middleware.Logger( + options.Logger, + ), + middleware.TraceContext, + 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 := chi.NewMux() + mux.Use(middlewares...) + + mux.Route(options.Config.HTTP.Root, func(r chi.Router) { + // the routes for the guestauth service will be added here + }) + + err = micro.RegisterHandler(newService.Server(), mux) + if err != nil { + options.Logger.Fatal().Err(err).Msg("failed to register the handler") + } + + newService.Init() + return newService, nil +} diff --git a/services/guestauth/pkg/service/options.go b/services/guestauth/pkg/service/options.go new file mode 100644 index 0000000000..629d54ec54 --- /dev/null +++ b/services/guestauth/pkg/service/options.go @@ -0,0 +1,39 @@ +package service + +import ( + "context" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" +) + +// Option for the guestauth service +type Option func(*Options) + +// Options for the guestauth service +type Options struct { + Logger log.Logger + Config *config.Config + Context context.Context +} + +// Logger configures a logger for the guestauth service +func Logger(log log.Logger) Option { + return func(o *Options) { + o.Logger = log + } +} + +// Config adds the config for the guestauth service +func Config(c *config.Config) Option { + return func(o *Options) { + o.Config = c + } +} + +// Context adds a context for the guestauth service +func Context(ctx context.Context) Option { + return func(o *Options) { + o.Context = ctx + } +} diff --git a/services/guestauth/pkg/service/service.go b/services/guestauth/pkg/service/service.go new file mode 100644 index 0000000000..63a814ee7a --- /dev/null +++ b/services/guestauth/pkg/service/service.go @@ -0,0 +1,52 @@ +package service + +import ( + "context" + "sync/atomic" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" +) + +// GuestauthService is the service responsible for creating and validating guest login tokens. +type GuestauthService struct { + log log.Logger + cfg *config.Config + ctx context.Context + stopCh chan struct{} + stopped atomic.Bool +} + +// New returns a guestauth service +func New(opts ...Option) (*GuestauthService, error) { + o := &Options{} + for _, opt := range opts { + opt(o) + } + + s := &GuestauthService{ + log: o.Logger, + cfg: o.Config, + ctx: o.Context, + stopCh: make(chan struct{}, 1), + } + + return s, nil +} + +// Run runs the service +func (s *GuestauthService) Run() error { + s.log.Info().Msg("starting guestauth service") + + <-s.stopCh + + s.log.Info().Msg("guestauth service stopped") + return nil +} + +// Close shuts down the service +func (s *GuestauthService) Close() { + if s.stopped.CompareAndSwap(false, true) { + close(s.stopCh) + } +} From 0448b456b0f98b5fb6a590d272e3a08cad67fdfa Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Tue, 22 Sep 2026 15:50:06 +0200 Subject: [PATCH 02/32] feat(guestauth): added consumer and some basic event handling logic with stubs --- services/guestauth/pkg/command/server.go | 36 +++- services/guestauth/pkg/config/config.go | 2 + .../pkg/config/defaults/defaultconfig.go | 1 + .../guestauth/pkg/service/events/options.go | 55 ++++++ .../guestauth/pkg/service/events/service.go | 170 ++++++++++++++++++ services/guestauth/pkg/service/options.go | 39 ---- services/guestauth/pkg/service/service.go | 52 ------ 7 files changed, 259 insertions(+), 96 deletions(-) create mode 100644 services/guestauth/pkg/service/events/options.go create mode 100644 services/guestauth/pkg/service/events/service.go delete mode 100644 services/guestauth/pkg/service/options.go delete mode 100644 services/guestauth/pkg/service/service.go diff --git a/services/guestauth/pkg/command/server.go b/services/guestauth/pkg/command/server.go index e918e46ea7..363e3cc04d 100644 --- a/services/guestauth/pkg/command/server.go +++ b/services/guestauth/pkg/command/server.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/opencloud-eu/opencloud/pkg/config/configlog" + "github.com/opencloud-eu/opencloud/pkg/generators" "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/runner" "github.com/opencloud-eu/opencloud/pkg/version" @@ -15,9 +16,17 @@ import ( "github.com/opencloud-eu/opencloud/services/guestauth/pkg/metrics" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/server/debug" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/server/http" - svc "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service" + svcEvents "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/events" + "github.com/opencloud-eu/reva/v2/pkg/events" + "github.com/opencloud-eu/reva/v2/pkg/events/stream" ) +var _registeredEvents = []events.Unmarshaller{ + events.ShareCreated{}, + events.ShareRemoved{}, + events.ShareExpired{}, +} + // Server is the entrypoint for the server command. func Server(cfg *config.Config) *cobra.Command { return &cobra.Command{ @@ -57,10 +66,27 @@ func Server(cfg *config.Config) *cobra.Command { } if !cfg.Events.Disabled { - guestAuth, err := svc.New( - svc.Logger(logger), - svc.Context(ctx), - svc.Config(cfg), + connName := generators.GenerateConnectionName(cfg.Service.Name, generators.NTypeBus) + evStream, err := stream.NatsFromConfig(connName, false, stream.NatsConfig{ + Endpoint: cfg.Events.Endpoint, + Cluster: cfg.Events.Cluster, + EnableTLS: cfg.Events.EnableTLS, + TLSInsecure: cfg.Events.TLSInsecure, + TLSRootCACertificate: cfg.Events.TLSRootCACertificate, + AuthUsername: cfg.Events.AuthUsername, + AuthPassword: cfg.Events.AuthPassword, + }) + if err != nil { + logger.Error().Err(err).Msg("Failed to initialize event stream") + return err + } + + guestAuth, err := svcEvents.New( + evStream, + svcEvents.Logger(logger), + svcEvents.Context(ctx), + svcEvents.RegisteredEvents(_registeredEvents), + svcEvents.NumConsumers(cfg.NumConsumers), ) if err != nil { logger.Error().Err(err).Str("transport", "event").Msg("Failed to initialize server") diff --git a/services/guestauth/pkg/config/config.go b/services/guestauth/pkg/config/config.go index a9b055102e..e1386235f1 100644 --- a/services/guestauth/pkg/config/config.go +++ b/services/guestauth/pkg/config/config.go @@ -26,6 +26,8 @@ type Config struct { ServiceAccount ServiceAccount `yaml:"service_account"` + NumConsumers int `yaml:"num_consumers" env:"GUESTAUTH_NUM_CONSUMERS" desc:"The amount of concurrent event consumers to start. Event consumers are used for processing events. Multiple consumers increase parallelisation, but will also increase CPU and memory demands." introductionVersion:"1.0.0"` + Context context.Context `yaml:"-"` } diff --git a/services/guestauth/pkg/config/defaults/defaultconfig.go b/services/guestauth/pkg/config/defaults/defaultconfig.go index ae0b054002..4cdd9dea2d 100644 --- a/services/guestauth/pkg/config/defaults/defaultconfig.go +++ b/services/guestauth/pkg/config/defaults/defaultconfig.go @@ -26,6 +26,7 @@ func DefaultConfig() *config.Config { Service: config.Service{ Name: "guestauth", }, + NumConsumers: 1, Events: config.Events{ Endpoint: "127.0.0.1:9233", Cluster: "opencloud-cluster", diff --git a/services/guestauth/pkg/service/events/options.go b/services/guestauth/pkg/service/events/options.go new file mode 100644 index 0000000000..eb95ba1d95 --- /dev/null +++ b/services/guestauth/pkg/service/events/options.go @@ -0,0 +1,55 @@ +package events + +import ( + "context" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/reva/v2/pkg/events" +) + +// Option for the guestauth service +type Option func(*Options) + +// Options for the guestauth service +type Options struct { + Context context.Context + Logger log.Logger + Stream events.Stream + RegisteredEvents []events.Unmarshaller + NumConsumers int +} + +// Context configures a context for the guestauth service +func Context(ctx context.Context) Option { + return func(o *Options) { + o.Context = ctx + } +} + +// Logger configures a logger for the guestauth service +func Logger(log log.Logger) Option { + return func(o *Options) { + o.Logger = log + } +} + +// Stream configures an event stream for the guestauth service +func Stream(s events.Stream) Option { + return func(o *Options) { + o.Stream = s + } +} + +// RegisteredEvents registers the events the service should listen to +func RegisteredEvents(e []events.Unmarshaller) Option { + return func(o *Options) { + o.RegisteredEvents = e + } +} + +// NumConsumers configures the amount of concurrent event consumers +func NumConsumers(num int) Option { + return func(o *Options) { + o.NumConsumers = num + } +} diff --git a/services/guestauth/pkg/service/events/service.go b/services/guestauth/pkg/service/events/service.go new file mode 100644 index 0000000000..2a0bf5af71 --- /dev/null +++ b/services/guestauth/pkg/service/events/service.go @@ -0,0 +1,170 @@ +package events + +import ( + "context" + "sync" + "sync/atomic" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/reva/v2/pkg/events" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" +) + +var tracer trace.Tracer + +func init() { + tracer = otel.Tracer("github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/events") +} + +var ( + _numConsumersDefault = 1 +) + +// GuestauthService consumes events for the guestauth service +type GuestauthService struct { + ctx context.Context + log log.Logger + stream events.Stream + + numConsumers int + + events []events.Unmarshaller + + stopCh chan struct{} + stopped *atomic.Bool +} + +// New creates a new GuestauthService +func New(stream events.Stream, opts ...Option) (*GuestauthService, error) { + o := &Options{ + NumConsumers: _numConsumersDefault, + } + for _, opt := range opts { + opt(o) + } + + s := &GuestauthService{ + ctx: o.Context, + log: o.Logger, + stream: stream, + events: o.RegisteredEvents, + numConsumers: o.NumConsumers, + stopCh: make(chan struct{}, 1), + stopped: new(atomic.Bool), + } + + return s, nil +} + +// Run to fulfil Runner interface +func (s *GuestauthService) Run() error { + ch, err := events.Consume(s.stream, "guestauth", s.events...) + if err != nil { + return err + } + + var wg sync.WaitGroup + ctx, cancel := context.WithCancel(s.ctx) + defer cancel() + + s.log.Debug().Int("worker.count", s.numConsumers). + Str("messaging.consumer.group.name", "guestauth"). + Str("messaging.system", "nats"). + Str("messaging.operation.name", "receive"). + Msg("starting event processing workers") + + // start workers + for i := range s.numConsumers { + wg.Add(1) + go func(workerID int) { + defer wg.Done() + for { + select { + case <-ctx.Done(): + return + case e, ok := <-ch: + if !ok { + return + } + if err := s.processEvent(e); err != nil { + s.log.Error().Err(err). + Int("worker", workerID). + Interface("event", e). + Msg("failed to process event") + } + } + } + }(i) + } + + // wait for stop signal + <-s.stopCh + cancel() // signal workers to stop + wg.Wait() + + return nil +} + +// Close will make the service to stop processing, so the `Run` +// method can finish. +func (s *GuestauthService) Close() { + if s.stopped.CompareAndSwap(false, true) { + close(s.stopCh) + } +} + +// processEvent dispatches an event to the matching handler. +func (s *GuestauthService) processEvent(e events.Event) error { + ctx := e.GetTraceContext(s.ctx) + ctx, span := tracer.Start(ctx, "processEvent") + defer span.End() + + s.log.Debug().Interface("event", e).Msg("processing event") + switch ev := e.Event.(type) { + case events.ShareCreated: + return s.handleShareCreated(ctx, ev) + case events.ShareRemoved: + return s.handleShareRemoved(ctx, ev) + case events.ShareExpired: + return s.handleShareExpired(ctx, ev) + default: + s.log.Warn(). + Str("eventtype", e.Type). + Msg("unhandled event") + } + + return nil +} + +// handleShareCreated handles a share created event. +func (s *GuestauthService) handleShareCreated(ctx context.Context, ev events.ShareCreated) error { + _, span := tracer.Start(ctx, "handleShareCreated") + defer span.End() + + s.log.Debug().Interface("event", ev).Msg("share created event received") + + return nil +} + +// handleShareRemoved handles a share removed event. +func (s *GuestauthService) handleShareRemoved(ctx context.Context, ev events.ShareRemoved) error { + _, span := tracer.Start(ctx, "handleShareRemoved") + defer span.End() + + s.log.Debug().Interface("event", ev).Msg("share removed event received") + // the cleanup of the guest token is implemented in a later step + + return nil +} + +// handleShareExpired handles a share expired event. +func (s *GuestauthService) handleShareExpired(ctx context.Context, ev events.ShareExpired) error { + _, span := tracer.Start(ctx, "handleShareExpired") + defer span.End() + + s.log.Debug().Interface("event", ev).Msg("share expired event received") + // the cleanup of the guest token is implemented in a later step + + return nil +} diff --git a/services/guestauth/pkg/service/options.go b/services/guestauth/pkg/service/options.go deleted file mode 100644 index 629d54ec54..0000000000 --- a/services/guestauth/pkg/service/options.go +++ /dev/null @@ -1,39 +0,0 @@ -package service - -import ( - "context" - - "github.com/opencloud-eu/opencloud/pkg/log" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" -) - -// Option for the guestauth service -type Option func(*Options) - -// Options for the guestauth service -type Options struct { - Logger log.Logger - Config *config.Config - Context context.Context -} - -// Logger configures a logger for the guestauth service -func Logger(log log.Logger) Option { - return func(o *Options) { - o.Logger = log - } -} - -// Config adds the config for the guestauth service -func Config(c *config.Config) Option { - return func(o *Options) { - o.Config = c - } -} - -// Context adds a context for the guestauth service -func Context(ctx context.Context) Option { - return func(o *Options) { - o.Context = ctx - } -} diff --git a/services/guestauth/pkg/service/service.go b/services/guestauth/pkg/service/service.go deleted file mode 100644 index 63a814ee7a..0000000000 --- a/services/guestauth/pkg/service/service.go +++ /dev/null @@ -1,52 +0,0 @@ -package service - -import ( - "context" - "sync/atomic" - - "github.com/opencloud-eu/opencloud/pkg/log" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" -) - -// GuestauthService is the service responsible for creating and validating guest login tokens. -type GuestauthService struct { - log log.Logger - cfg *config.Config - ctx context.Context - stopCh chan struct{} - stopped atomic.Bool -} - -// New returns a guestauth service -func New(opts ...Option) (*GuestauthService, error) { - o := &Options{} - for _, opt := range opts { - opt(o) - } - - s := &GuestauthService{ - log: o.Logger, - cfg: o.Config, - ctx: o.Context, - stopCh: make(chan struct{}, 1), - } - - return s, nil -} - -// Run runs the service -func (s *GuestauthService) Run() error { - s.log.Info().Msg("starting guestauth service") - - <-s.stopCh - - s.log.Info().Msg("guestauth service stopped") - return nil -} - -// Close shuts down the service -func (s *GuestauthService) Close() { - if s.stopped.CompareAndSwap(false, true) { - close(s.stopCh) - } -} From c5e52f97c6d3e5e8511535cacbae7525c8d1921d Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Tue, 22 Sep 2026 19:28:54 +0200 Subject: [PATCH 03/32] feat(guestauth): token service --- .../guestauth/pkg/service/events/service.go | 3 +- services/guestauth/pkg/service/token/token.go | 55 ++++++++ .../guestauth/pkg/service/token/token_test.go | 118 ++++++++++++++++++ 3 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 services/guestauth/pkg/service/token/token.go create mode 100644 services/guestauth/pkg/service/token/token_test.go diff --git a/services/guestauth/pkg/service/events/service.go b/services/guestauth/pkg/service/events/service.go index 2a0bf5af71..b399cb85ff 100644 --- a/services/guestauth/pkg/service/events/service.go +++ b/services/guestauth/pkg/service/events/service.go @@ -121,6 +121,7 @@ func (s *GuestauthService) processEvent(e events.Event) error { defer span.End() s.log.Debug().Interface("event", e).Msg("processing event") + switch ev := e.Event.(type) { case events.ShareCreated: return s.handleShareCreated(ctx, ev) @@ -153,7 +154,6 @@ func (s *GuestauthService) handleShareRemoved(ctx context.Context, ev events.Sha defer span.End() s.log.Debug().Interface("event", ev).Msg("share removed event received") - // the cleanup of the guest token is implemented in a later step return nil } @@ -164,7 +164,6 @@ func (s *GuestauthService) handleShareExpired(ctx context.Context, ev events.Sha defer span.End() s.log.Debug().Interface("event", ev).Msg("share expired event received") - // the cleanup of the guest token is implemented in a later step return nil } diff --git a/services/guestauth/pkg/service/token/token.go b/services/guestauth/pkg/service/token/token.go new file mode 100644 index 0000000000..cf0a3a7673 --- /dev/null +++ b/services/guestauth/pkg/service/token/token.go @@ -0,0 +1,55 @@ +package token + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "strings" +) + +const ( + tokenVersion = "v1" + secretLength = 32 + tokenParts = 3 +) + +var ErrInvalidToken = errors.New("invalid token") + +type TokenService struct{} + +func NewTokenService() *TokenService { + return &TokenService{} +} + +func (s *TokenService) Generate(shareID string) (string, error) { + secretBytes := make([]byte, secretLength) + if _, err := rand.Read(secretBytes); err != nil { + return "", fmt.Errorf("could not generate random secret: %w", err) + } + + return strings.Join([]string{ + tokenVersion, + hash(shareID), + base64.RawURLEncoding.EncodeToString(secretBytes), + }, "."), nil +} + +func (s *TokenService) Verify(tokenString string, storedSecretHash string) error { + parts := strings.Split(tokenString, ".") + if len(parts) != tokenParts || parts[0] != tokenVersion || parts[1] == "" || parts[2] == "" { + return ErrInvalidToken + } + + if hash(parts[2]) != storedSecretHash { + return ErrInvalidToken + } + + return nil +} + +func hash(s string) string { + h := sha256.Sum256([]byte(s)) + return base64.RawURLEncoding.EncodeToString(h[:]) +} diff --git a/services/guestauth/pkg/service/token/token_test.go b/services/guestauth/pkg/service/token/token_test.go new file mode 100644 index 0000000000..5f8d2ce07b --- /dev/null +++ b/services/guestauth/pkg/service/token/token_test.go @@ -0,0 +1,118 @@ +package token + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testShareID = "e0123456-7890-abcd-ef01-234567890abc" + +func TestGenerate(t *testing.T) { + svc := NewTokenService() + + tok, err := svc.Generate(testShareID) + require.NoError(t, err) + + parts := strings.Split(tok, ".") + require.Len(t, parts, tokenParts) + assert.Equal(t, tokenVersion, parts[0]) + assert.Equal(t, hash(testShareID), parts[1]) + assert.NotEmpty(t, parts[2]) +} + +func TestGenerateDeterminism(t *testing.T) { + svc := NewTokenService() + + tok1, err := svc.Generate(testShareID) + require.NoError(t, err) + tok2, err := svc.Generate(testShareID) + require.NoError(t, err) + + assert.Equal(t, hash(testShareID), strings.Split(tok1, ".")[1]) + assert.Equal(t, hash(testShareID), strings.Split(tok2, ".")[1]) + assert.NotEqual(t, tok1, tok2) + + other, err := svc.Generate("9f9f9f9-9f9f-9f9f-9f9f-9f9f9f9f9f9f") + require.NoError(t, err) + assert.NotEqual(t, strings.Split(tok1, ".")[1], strings.Split(other, ".")[1]) +} + +func TestVerify(t *testing.T) { + svc := NewTokenService() + + tok, err := svc.Generate(testShareID) + require.NoError(t, err) + + hashPart := strings.Split(tok, ".")[1] + secretPart := strings.Split(tok, ".")[2] + storedSecretHash := hash(secretPart) + + tests := []struct { + name string + token string + storedSecretHash string + expectError bool + }{ + { + name: "valid token", + token: tok, + storedSecretHash: storedSecretHash, + }, + { + name: "tampered secret", + token: "v1." + hashPart + ".tampered", + storedSecretHash: storedSecretHash, + expectError: true, + }, + { + name: "wrong stored secret", + token: tok, + storedSecretHash: hash("other-secret"), + expectError: true, + }, + { + name: "wrong version", + token: "v2." + hashPart + "." + secretPart, + storedSecretHash: storedSecretHash, + expectError: true, + }, + { + name: "missing version", + token: hashPart + "." + secretPart, + storedSecretHash: storedSecretHash, + expectError: true, + }, + { + name: "too many parts", + token: "v1." + hashPart + "." + secretPart + ".extra", + storedSecretHash: storedSecretHash, + expectError: true, + }, + { + name: "empty hash", + token: "v1.." + secretPart, + storedSecretHash: storedSecretHash, + expectError: true, + }, + { + name: "empty secret", + token: "v1." + hashPart + ".", + storedSecretHash: storedSecretHash, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := svc.Verify(tt.token, tt.storedSecretHash) + if tt.expectError { + assert.ErrorIs(t, err, ErrInvalidToken) + } else { + assert.NoError(t, err) + } + }) + } +} From 06f7d12c4caa39eb426158395b19e5aa136ffccc Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Wed, 23 Sep 2026 13:42:13 +0200 Subject: [PATCH 04/32] feat(guestauth): storage implementation for guest auth tokens --- services/guestauth/pkg/config/config.go | 6 + .../pkg/config/defaults/defaultconfig.go | 6 + .../pkg/service/storage/file_storage.go | 131 ++++++++++++++++++ .../pkg/service/storage/file_storage_test.go | 101 ++++++++++++++ .../guestauth/pkg/service/storage/storage.go | 27 ++++ 5 files changed, 271 insertions(+) create mode 100644 services/guestauth/pkg/service/storage/file_storage.go create mode 100644 services/guestauth/pkg/service/storage/file_storage_test.go create mode 100644 services/guestauth/pkg/service/storage/storage.go diff --git a/services/guestauth/pkg/config/config.go b/services/guestauth/pkg/config/config.go index e1386235f1..aa503f2738 100644 --- a/services/guestauth/pkg/config/config.go +++ b/services/guestauth/pkg/config/config.go @@ -22,6 +22,7 @@ type Config struct { GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"` HTTP HTTP `yaml:"http"` + Storage Storage `yaml:"storage"` TokenManager *TokenManager `yaml:"token_manager"` ServiceAccount ServiceAccount `yaml:"service_account"` @@ -67,6 +68,11 @@ type HTTP struct { TLS shared.HTTPServiceTLS `yaml:"tls"` } +// Storage defines the configuration for the token storage. +type Storage struct { + RootDirectory string `yaml:"root_directory" env:"GUESTAUTH_TOKENS_STORAGE_ROOT" desc:"The directory where the guest share tokens are stored. If not defined, the root directory derives from $OC_BASE_DATA_PATH/guestauth." introductionVersion:"1.0.0"` +} + // TokenManager is the config for using the reva token manager type TokenManager struct { JWTSecret string `yaml:"jwt_secret" env:"OC_JWT_SECRET;GUESTAUTH_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"1.0.0"` diff --git a/services/guestauth/pkg/config/defaults/defaultconfig.go b/services/guestauth/pkg/config/defaults/defaultconfig.go index 4cdd9dea2d..80ebf45869 100644 --- a/services/guestauth/pkg/config/defaults/defaultconfig.go +++ b/services/guestauth/pkg/config/defaults/defaultconfig.go @@ -1,6 +1,9 @@ package defaults import ( + "path" + + "github.com/opencloud-eu/opencloud/pkg/config/defaults" "github.com/opencloud-eu/opencloud/pkg/shared" "github.com/opencloud-eu/opencloud/pkg/structs" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" @@ -44,6 +47,9 @@ func DefaultConfig() *config.Config { AllowCredentials: true, }, }, + Storage: config.Storage{ + RootDirectory: path.Join(defaults.BaseDataPath(), "guestauth"), + }, } } diff --git a/services/guestauth/pkg/service/storage/file_storage.go b/services/guestauth/pkg/service/storage/file_storage.go new file mode 100644 index 0000000000..97fa06e793 --- /dev/null +++ b/services/guestauth/pkg/service/storage/file_storage.go @@ -0,0 +1,131 @@ +package storage + +import ( + "encoding/json" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "sync" +) + +func NewFileStorage(root string) *FileStorage { + return &FileStorage{ + root: root, + } +} + +type FileStorage struct { + root string + mu sync.Mutex +} + +const dirPerm = 0700 + +func (s *FileStorage) Add(rec Record) error { + s.mu.Lock() + defer s.mu.Unlock() + + return s.add(rec) +} + +// Get returns the record for the given share id hash. +func (s *FileStorage) Get(shareIDHash string) (Record, error) { + return s.get(shareIDHash) +} + +func (s *FileStorage) Remove(shareIDHash string) error { + s.mu.Lock() + defer s.mu.Unlock() + + p := s.path(shareIDHash) + if _, err := os.Stat(p); err != nil { + if errors.Is(err, fs.ErrNotExist) { + return ErrNotFound + } + return err + } + + if err := os.Remove(p); err != nil { + return err + } + + return nil +} + +func (s *FileStorage) Redeem(shareIDHash string) error { + s.mu.Lock() + defer s.mu.Unlock() + + rec, err := s.get(shareIDHash) + if err != nil { + return err + } + + rec.Redeemed = true + return s.add(rec) +} + +func (s *FileStorage) add(rec Record) error { + data, err := json.Marshal(rec) + if err != nil { + return err + } + + p := s.path(rec.ShareIDHash) + dir := filepath.Dir(p) + if err := os.MkdirAll(dir, dirPerm); err != nil { + return fmt.Errorf("could not create directory %s: %w", dir, err) + } + + // Create temporary file is needed to ensure that if something is wrong during write we will not have + // a corupted file on disk which can be later treated as a valid file which contains a token record. + f, err := os.CreateTemp(dir, "tmpguestauth") + if err != nil { + return fmt.Errorf("could not create temporary file for %s: %w", rec.ShareIDHash, err) + } + defer f.Close() + + if _, writeErr := f.Write(data); writeErr != nil { + if remErr := os.Remove(f.Name()); remErr != nil { + return fmt.Errorf("could not cleanup temporary file for %s: %w", rec.ShareIDHash, remErr) + } + return fmt.Errorf("could not write temporary file for %s: %w", rec.ShareIDHash, writeErr) + } + + // just in case there is a simultan write of the identic file(record) + if synErr := f.Sync(); synErr != nil { + return fmt.Errorf("could not sync temporary file for %s: %w", rec.ShareIDHash, synErr) + } + + if renErr := os.Rename(f.Name(), p); renErr != nil { + if remErr := os.Remove(f.Name()); remErr != nil { + return fmt.Errorf("rename failed and could not cleanup temporary file for %s: %w", rec.ShareIDHash, remErr) + } + return fmt.Errorf("could not rename temporary file to %s: %w", p, renErr) + } + + return nil +} + +func (s *FileStorage) get(shareIDHash string) (Record, error) { + data, err := os.ReadFile(s.path(shareIDHash)) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return Record{}, ErrNotFound + } + return Record{}, err + } + + rec := Record{} + if err := json.Unmarshal(data, &rec); err != nil { + return Record{}, err + } + + return rec, nil +} + +func (s *FileStorage) path(shareIDHash string) string { + return filepath.Join(s.root, shareIDHash[:2], shareIDHash[2:4], shareIDHash[4:]+".json") +} diff --git a/services/guestauth/pkg/service/storage/file_storage_test.go b/services/guestauth/pkg/service/storage/file_storage_test.go new file mode 100644 index 0000000000..5a4ae1ec8a --- /dev/null +++ b/services/guestauth/pkg/service/storage/file_storage_test.go @@ -0,0 +1,101 @@ +package storage + +import ( + "strings" + "testing" + "time" + + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newRecord(shareID string) Record { + svc := token.NewTokenService() + tok, _ := svc.Generate(shareID) + + return Record{ + ShareID: shareID, + ShareIDHash: strings.Split(tok, ".")[1], + SecretHash: strings.Split(tok, ".")[2], + Expiry: time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC), + } +} + +func TestFileStorageAddGet(t *testing.T) { + dir := t.TempDir() + s := NewFileStorage(dir) + + rec := newRecord("e0123456-7890-abcd-ef01-234567890abc") + require.NoError(t, s.Add(rec)) + + got, err := s.Get(rec.ShareIDHash) + require.NoError(t, err) + assert.Equal(t, rec, got) +} + +func TestFileStorageGetMissing(t *testing.T) { + dir := t.TempDir() + s := NewFileStorage(dir) + + _, err := s.Get("doesnotexist") + assert.ErrorIs(t, err, ErrNotFound) +} + +func TestFileStorageAddOverwrites(t *testing.T) { + dir := t.TempDir() + s := NewFileStorage(dir) + + rec := newRecord("e0123456-7890-abcd-ef01-234567890abc") + require.NoError(t, s.Add(rec)) + + rec.SecretHash = "other" + require.NoError(t, s.Add(rec)) + + got, err := s.Get(rec.ShareIDHash) + require.NoError(t, err) + assert.Equal(t, "other", got.SecretHash) +} + +func TestFileStorageRemove(t *testing.T) { + dir := t.TempDir() + s := NewFileStorage(dir) + + rec := newRecord("e0123456-7890-abcd-ef01-234567890abc") + require.NoError(t, s.Add(rec)) + + require.NoError(t, s.Remove(rec.ShareIDHash)) + + _, err := s.Get(rec.ShareIDHash) + assert.ErrorIs(t, err, ErrNotFound) +} + +func TestFileStorageRemoveMissing(t *testing.T) { + dir := t.TempDir() + s := NewFileStorage(dir) + + err := s.Remove("doesnotexist") + assert.ErrorIs(t, err, ErrNotFound) +} + +func TestFileStorageRedeem(t *testing.T) { + dir := t.TempDir() + s := NewFileStorage(dir) + + rec := newRecord("e0123456-7890-abcd-ef01-234567890abc") + require.NoError(t, s.Add(rec)) + + require.NoError(t, s.Redeem(rec.ShareIDHash)) + + got, err := s.Get(rec.ShareIDHash) + require.NoError(t, err) + assert.True(t, got.Redeemed) +} + +func TestFileStorageRedeemMissing(t *testing.T) { + dir := t.TempDir() + s := NewFileStorage(dir) + + err := s.Redeem("doesnotexist") + assert.ErrorIs(t, err, ErrNotFound) +} diff --git a/services/guestauth/pkg/service/storage/storage.go b/services/guestauth/pkg/service/storage/storage.go new file mode 100644 index 0000000000..91389d067e --- /dev/null +++ b/services/guestauth/pkg/service/storage/storage.go @@ -0,0 +1,27 @@ +package storage + +import ( + "errors" + "time" +) + +// ErrNotFound is returned when a record does not exist in the storage. +var ErrNotFound = errors.New("record not found") + +// Record holds the data persisted for a guest share token. +type Record struct { + ShareID string `json:"shareid"` + ShareIDHash string `json:"shareidhash"` + SecretHash string `json:"secrethash"` + Expiry time.Time `json:"expiry,omitzero"` + Redeemed bool `json:"redeemed"` +} + +// Storage is the interface for persisting token records. Implementations need +// to be safe for concurrent use. +type Storage interface { + Add(rec Record) error + Get(shareIDHash string) (Record, error) + Remove(shareIDHash string) error + Redeem(shareIDHash string) error +} From a60ec8cbc35d173212f025cb88a6555c8bcd5924 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Wed, 23 Sep 2026 19:48:37 +0200 Subject: [PATCH 05/32] feat(guestauth): handler for redeem action and http service --- services/guestauth/pkg/command/server.go | 12 +++ services/guestauth/pkg/server/http/option.go | 8 ++ services/guestauth/pkg/server/http/redeem.go | 53 +++++++++++++ services/guestauth/pkg/server/http/server.go | 2 +- services/guestauth/pkg/service/http/option.go | 31 ++++++++ .../guestauth/pkg/service/http/service.go | 56 ++++++++++++++ .../pkg/service/http/service_test.go | 74 +++++++++++++++++++ .../pkg/service/storage/file_storage_test.go | 2 +- services/guestauth/pkg/service/token/token.go | 17 ++++- .../guestauth/pkg/service/token/token_test.go | 10 +-- 10 files changed, 254 insertions(+), 11 deletions(-) create mode 100644 services/guestauth/pkg/server/http/redeem.go create mode 100644 services/guestauth/pkg/service/http/option.go create mode 100644 services/guestauth/pkg/service/http/service.go create mode 100644 services/guestauth/pkg/service/http/service_test.go diff --git a/services/guestauth/pkg/command/server.go b/services/guestauth/pkg/command/server.go index 363e3cc04d..f27f75f75d 100644 --- a/services/guestauth/pkg/command/server.go +++ b/services/guestauth/pkg/command/server.go @@ -17,6 +17,9 @@ import ( "github.com/opencloud-eu/opencloud/services/guestauth/pkg/server/debug" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/server/http" svcEvents "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/events" + svcHttp "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/http" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" "github.com/opencloud-eu/reva/v2/pkg/events" "github.com/opencloud-eu/reva/v2/pkg/events/stream" ) @@ -45,11 +48,20 @@ func Server(cfg *config.Config) *cobra.Command { mtrcs := metrics.New() mtrcs.BuildInfo.WithLabelValues(version.GetString()).Set(1) + tokenSvc := token.NewTokenService() + store := storage.NewFileStorage(cfg.Storage.RootDirectory) + redeemSvc, err := svcHttp.NewService(tokenSvc, store) + if err != nil { + logger.Error().Err(err).Msg("Failed to initialize http service") + return err + } + if !cfg.HTTP.Disabled { server, err := http.Server( http.Logger(logger), http.Context(ctx), http.Config(cfg), + http.Service(redeemSvc), ) if err != nil { logger.Info(). diff --git a/services/guestauth/pkg/server/http/option.go b/services/guestauth/pkg/server/http/option.go index 1dca47e974..178ae6132a 100644 --- a/services/guestauth/pkg/server/http/option.go +++ b/services/guestauth/pkg/server/http/option.go @@ -16,6 +16,7 @@ type Options struct { Logger log.Logger Context context.Context Config *config.Config + Service RedeemService Flags []pflag.Flag } @@ -51,6 +52,13 @@ func Config(val *config.Config) Option { } } +// Service provides a function to set the service option. +func Service(val RedeemService) Option { + return func(o *Options) { + o.Service = val + } +} + // Flags provides a function to set the flags option. func Flags(flags ...pflag.Flag) Option { return func(o *Options) { diff --git a/services/guestauth/pkg/server/http/redeem.go b/services/guestauth/pkg/server/http/redeem.go new file mode 100644 index 0000000000..965217c8bc --- /dev/null +++ b/services/guestauth/pkg/server/http/redeem.go @@ -0,0 +1,53 @@ +package http + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/opencloud-eu/opencloud/pkg/log" + svchttp "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/http" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" + token "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" +) + +type RedeemService interface { + VerifyToken(tokenString string) (storage.Record, error) +} + +type RedeemRequest struct { + Token string `json:"token"` +} + +func RedeemHandler(log log.Logger, s RedeemService) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var req RedeemRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + log.Debug().Err(err).Msg("request body is malformed") + w.WriteHeader(http.StatusBadRequest) + return + } + + _, err := s.VerifyToken(req.Token) + if err != nil { + switch { + case errors.Is(err, svchttp.ErrExpired) || errors.Is(err, svchttp.ErrAlreadyRedeemed): + log.Debug().Err(err).Msg("token expired or already redeemed") + w.WriteHeader(http.StatusGone) + case errors.Is(err, storage.ErrNotFound): + log.Debug().Err(err).Msg("no token record found") + w.WriteHeader(http.StatusNotFound) + case errors.Is(err, token.ErrInvalidToken): + log.Debug().Err(err).Msg("token is invalid") + w.WriteHeader(http.StatusUnauthorized) + default: + log.Error().Err(err).Msg("error verifying token") + w.WriteHeader(http.StatusInternalServerError) + } + return + } + + // session create should be here + w.WriteHeader(http.StatusOK) + } +} diff --git a/services/guestauth/pkg/server/http/server.go b/services/guestauth/pkg/server/http/server.go index 8b0c953d6e..a6cb3ac543 100644 --- a/services/guestauth/pkg/server/http/server.go +++ b/services/guestauth/pkg/server/http/server.go @@ -56,7 +56,7 @@ func Server(opts ...Option) (ohttp.Service, error) { mux.Use(middlewares...) mux.Route(options.Config.HTTP.Root, func(r chi.Router) { - // the routes for the guestauth service will be added here + r.Post("/v1beta1/guestInvitations/redeem", RedeemHandler(options.Logger, options.Service)) }) err = micro.RegisterHandler(newService.Server(), mux) diff --git a/services/guestauth/pkg/service/http/option.go b/services/guestauth/pkg/service/http/option.go new file mode 100644 index 0000000000..8eb32916d7 --- /dev/null +++ b/services/guestauth/pkg/service/http/option.go @@ -0,0 +1,31 @@ +package http + +import ( + "github.com/opencloud-eu/opencloud/pkg/log" +) + +// Option defines a single option function. +type Option func(o *Options) + +// Options defines the available options for this package. +type Options struct { + Logger log.Logger +} + +// newOptions initializes the available default options. +func newOptions(opts ...Option) Options { + opt := Options{} + + for _, o := range opts { + o(&opt) + } + + return opt +} + +// Logger provides a function to set the logger option. +func Logger(val log.Logger) Option { + return func(o *Options) { + o.Logger = val + } +} diff --git a/services/guestauth/pkg/service/http/service.go b/services/guestauth/pkg/service/http/service.go new file mode 100644 index 0000000000..34592070d7 --- /dev/null +++ b/services/guestauth/pkg/service/http/service.go @@ -0,0 +1,56 @@ +package http + +import ( + "errors" + "time" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" +) + +var ErrExpired = errors.New("token expired") +var ErrAlreadyRedeemed = errors.New("token already redeemed") + +func NewService(tokenSvc *token.TokenService, store storage.Storage, opts ...Option) (*svc, error) { + o := newOptions(opts...) + + return &svc{ + log: o.Logger, + tokenSvc: tokenSvc, + store: store, + }, nil +} + +// svc provides the logic behind the http endpoints of the guestauth service. +type svc struct { + log log.Logger + tokenSvc *token.TokenService + store storage.Storage +} + +func (s *svc) VerifyToken(tokenString string) (storage.Record, error) { + shareIDHash, err := s.tokenSvc.ShareIDHash(tokenString) + if err != nil { + return storage.Record{}, err + } + + rec, err := s.store.Get(shareIDHash) + if err != nil { + return storage.Record{}, err + } + + if err := s.tokenSvc.Verify(tokenString, rec.SecretHash); err != nil { + return storage.Record{}, err + } + + if !rec.Expiry.IsZero() && rec.Expiry.Before(time.Now()) { + return storage.Record{}, ErrExpired + } + + if rec.Redeemed { + return storage.Record{}, ErrAlreadyRedeemed + } + + return rec, nil +} diff --git a/services/guestauth/pkg/service/http/service_test.go b/services/guestauth/pkg/service/http/service_test.go new file mode 100644 index 0000000000..15c6125d78 --- /dev/null +++ b/services/guestauth/pkg/service/http/service_test.go @@ -0,0 +1,74 @@ +package http + +import ( + "strings" + "testing" + "time" + + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testShareID = "e0123456-7890-abcd-ef01-234567890abc" + +func newToken(t *testing.T) (string, storage.Record) { + ts := token.NewTokenService() + tok, err := ts.Generate(testShareID) + require.NoError(t, err) + parts := strings.Split(tok, ".") + + rec := storage.Record{ + ShareID: testShareID, + ShareIDHash: parts[1], + SecretHash: ts.Hash(parts[2]), + Expiry: time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC), + } + + return tok, rec +} + +func newStorage(t *testing.T) storage.Storage { + return storage.NewFileStorage(t.TempDir()) +} + +func newService(t *testing.T, store storage.Storage) *svc { + s, err := NewService(token.NewTokenService(), store) + require.NoError(t, err) + + return s +} + +func TestVerifyTokenValid(t *testing.T) { + store := newStorage(t) + s := newService(t, store) + tok, rec := newToken(t) + require.NoError(t, store.Add(rec)) + + got, err := s.VerifyToken(tok) + require.NoError(t, err) + assert.Equal(t, rec, got) +} + +func TestVerifyTokenExpired(t *testing.T) { + store := newStorage(t) + s := newService(t, store) + tok, rec := newToken(t) + rec.Expiry = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) + require.NoError(t, store.Add(rec)) + + _, err := s.VerifyToken(tok) + assert.ErrorIs(t, err, ErrExpired) +} + +func TestVerifyTokenAlreadyRedeemed(t *testing.T) { + store := newStorage(t) + s := newService(t, store) + tok, rec := newToken(t) + require.NoError(t, store.Add(rec)) + require.NoError(t, store.Redeem(rec.ShareIDHash)) + + _, err := s.VerifyToken(tok) + assert.ErrorIs(t, err, ErrAlreadyRedeemed) +} diff --git a/services/guestauth/pkg/service/storage/file_storage_test.go b/services/guestauth/pkg/service/storage/file_storage_test.go index 5a4ae1ec8a..1ba349adba 100644 --- a/services/guestauth/pkg/service/storage/file_storage_test.go +++ b/services/guestauth/pkg/service/storage/file_storage_test.go @@ -17,7 +17,7 @@ func newRecord(shareID string) Record { return Record{ ShareID: shareID, ShareIDHash: strings.Split(tok, ".")[1], - SecretHash: strings.Split(tok, ".")[2], + SecretHash: svc.Hash(strings.Split(tok, ".")[2]), Expiry: time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC), } } diff --git a/services/guestauth/pkg/service/token/token.go b/services/guestauth/pkg/service/token/token.go index cf0a3a7673..fac4467aa2 100644 --- a/services/guestauth/pkg/service/token/token.go +++ b/services/guestauth/pkg/service/token/token.go @@ -31,7 +31,7 @@ func (s *TokenService) Generate(shareID string) (string, error) { return strings.Join([]string{ tokenVersion, - hash(shareID), + s.Hash(shareID), base64.RawURLEncoding.EncodeToString(secretBytes), }, "."), nil } @@ -42,14 +42,23 @@ func (s *TokenService) Verify(tokenString string, storedSecretHash string) error return ErrInvalidToken } - if hash(parts[2]) != storedSecretHash { + if s.Hash(parts[2]) != storedSecretHash { return ErrInvalidToken } return nil } -func hash(s string) string { - h := sha256.Sum256([]byte(s)) +func (s *TokenService) ShareIDHash(tokenString string) (string, error) { + parts := strings.Split(tokenString, ".") + if len(parts) != tokenParts || parts[0] != tokenVersion || parts[1] == "" || parts[2] == "" { + return "", ErrInvalidToken + } + + return parts[1], nil +} + +func (s *TokenService) Hash(str string) string { + h := sha256.Sum256([]byte(str)) return base64.RawURLEncoding.EncodeToString(h[:]) } diff --git a/services/guestauth/pkg/service/token/token_test.go b/services/guestauth/pkg/service/token/token_test.go index 5f8d2ce07b..6cb5aff09c 100644 --- a/services/guestauth/pkg/service/token/token_test.go +++ b/services/guestauth/pkg/service/token/token_test.go @@ -19,7 +19,7 @@ func TestGenerate(t *testing.T) { parts := strings.Split(tok, ".") require.Len(t, parts, tokenParts) assert.Equal(t, tokenVersion, parts[0]) - assert.Equal(t, hash(testShareID), parts[1]) + assert.Equal(t, svc.Hash(testShareID), parts[1]) assert.NotEmpty(t, parts[2]) } @@ -31,8 +31,8 @@ func TestGenerateDeterminism(t *testing.T) { tok2, err := svc.Generate(testShareID) require.NoError(t, err) - assert.Equal(t, hash(testShareID), strings.Split(tok1, ".")[1]) - assert.Equal(t, hash(testShareID), strings.Split(tok2, ".")[1]) + assert.Equal(t, svc.Hash(testShareID), strings.Split(tok1, ".")[1]) + assert.Equal(t, svc.Hash(testShareID), strings.Split(tok2, ".")[1]) assert.NotEqual(t, tok1, tok2) other, err := svc.Generate("9f9f9f9-9f9f-9f9f-9f9f-9f9f9f9f9f9f") @@ -48,7 +48,7 @@ func TestVerify(t *testing.T) { hashPart := strings.Split(tok, ".")[1] secretPart := strings.Split(tok, ".")[2] - storedSecretHash := hash(secretPart) + storedSecretHash := svc.Hash(secretPart) tests := []struct { name string @@ -70,7 +70,7 @@ func TestVerify(t *testing.T) { { name: "wrong stored secret", token: tok, - storedSecretHash: hash("other-secret"), + storedSecretHash: svc.Hash("other-secret"), expectError: true, }, { From 7af24c09a8c73c00a82cbfb336f04d67354fdb67 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Thu, 24 Sep 2026 15:57:24 +0200 Subject: [PATCH 06/32] feat(guestauth): handle events and a bit of cleanup --- pkg/events/events.go | 16 +++ services/guestauth/pkg/command/server.go | 17 +-- services/guestauth/pkg/server/http/option.go | 5 +- services/guestauth/pkg/server/http/redeem.go | 12 +- .../guestauth/pkg/service/events/handlers.go | 55 ++++++++ .../pkg/service/events/handlers_test.go | 70 ++++++++++ .../guestauth/pkg/service/events/options.go | 9 ++ .../guestauth/pkg/service/events/service.go | 50 ++------ .../pkg/service/guestauth/service.go | 82 ++++++++++++ .../{http => guestauth}/service_test.go | 39 +++--- services/guestauth/pkg/service/http/option.go | 31 ----- .../guestauth/pkg/service/http/service.go | 56 -------- .../pkg/service/storage/file_storage_test.go | 5 +- services/guestauth/pkg/service/token/token.go | 55 ++++---- .../guestauth/pkg/service/token/token_test.go | 121 ++++++++---------- 15 files changed, 368 insertions(+), 255 deletions(-) create mode 100644 services/guestauth/pkg/service/events/handlers.go create mode 100644 services/guestauth/pkg/service/events/handlers_test.go create mode 100644 services/guestauth/pkg/service/guestauth/service.go rename services/guestauth/pkg/service/{http => guestauth}/service_test.go (66%) delete mode 100644 services/guestauth/pkg/service/http/option.go delete mode 100644 services/guestauth/pkg/service/http/service.go diff --git a/pkg/events/events.go b/pkg/events/events.go index 3af491210f..79124ff233 100644 --- a/pkg/events/events.go +++ b/pkg/events/events.go @@ -5,6 +5,7 @@ import ( "time" user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" ) @@ -20,3 +21,18 @@ func (ResourceMention) Unmarshal(v []byte) (interface{}, error) { err := json.Unmarshal(v, &e) return e, err } + +type GuestTokenCreated struct { + ShareID *collaboration.ShareId + Sharer *user.UserId + ItemID *provider.ResourceId + ResourceName string + Token string + Timestamp time.Time +} + +func (GuestTokenCreated) Unmarshal(v []byte) (interface{}, error) { + e := GuestTokenCreated{} + err := json.Unmarshal(v, &e) + return e, err +} diff --git a/services/guestauth/pkg/command/server.go b/services/guestauth/pkg/command/server.go index f27f75f75d..8d4abd82c6 100644 --- a/services/guestauth/pkg/command/server.go +++ b/services/guestauth/pkg/command/server.go @@ -17,7 +17,7 @@ import ( "github.com/opencloud-eu/opencloud/services/guestauth/pkg/server/debug" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/server/http" svcEvents "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/events" - svcHttp "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/http" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" "github.com/opencloud-eu/reva/v2/pkg/events" @@ -50,18 +50,14 @@ func Server(cfg *config.Config) *cobra.Command { tokenSvc := token.NewTokenService() store := storage.NewFileStorage(cfg.Storage.RootDirectory) - redeemSvc, err := svcHttp.NewService(tokenSvc, store) - if err != nil { - logger.Error().Err(err).Msg("Failed to initialize http service") - return err - } + guestAuth := guestauth.NewGuestAuthService(tokenSvc, store) if !cfg.HTTP.Disabled { server, err := http.Server( http.Logger(logger), http.Context(ctx), http.Config(cfg), - http.Service(redeemSvc), + http.Service(guestAuth), ) if err != nil { logger.Info(). @@ -93,12 +89,13 @@ func Server(cfg *config.Config) *cobra.Command { return err } - guestAuth, err := svcEvents.New( + consumer, err := svcEvents.NewEventConsumer( evStream, svcEvents.Logger(logger), svcEvents.Context(ctx), svcEvents.RegisteredEvents(_registeredEvents), svcEvents.NumConsumers(cfg.NumConsumers), + svcEvents.GuestAuthService(guestAuth), ) if err != nil { logger.Error().Err(err).Str("transport", "event").Msg("Failed to initialize server") @@ -106,9 +103,9 @@ func Server(cfg *config.Config) *cobra.Command { } gr.Add(runner.New(cfg.Service.Name+".svc", func() error { - return guestAuth.Run() + return consumer.Run() }, func() { - guestAuth.Close() + consumer.Close() })) } else { logger.Info().Msg("event listening disabled, not starting event service") diff --git a/services/guestauth/pkg/server/http/option.go b/services/guestauth/pkg/server/http/option.go index 178ae6132a..bc897b2a14 100644 --- a/services/guestauth/pkg/server/http/option.go +++ b/services/guestauth/pkg/server/http/option.go @@ -5,6 +5,7 @@ import ( "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" "github.com/spf13/pflag" ) @@ -16,7 +17,7 @@ type Options struct { Logger log.Logger Context context.Context Config *config.Config - Service RedeemService + Service *guestauth.GuestAuthService Flags []pflag.Flag } @@ -53,7 +54,7 @@ func Config(val *config.Config) Option { } // Service provides a function to set the service option. -func Service(val RedeemService) Option { +func Service(val *guestauth.GuestAuthService) Option { return func(o *Options) { o.Service = val } diff --git a/services/guestauth/pkg/server/http/redeem.go b/services/guestauth/pkg/server/http/redeem.go index 965217c8bc..4e632f2186 100644 --- a/services/guestauth/pkg/server/http/redeem.go +++ b/services/guestauth/pkg/server/http/redeem.go @@ -6,20 +6,18 @@ import ( "net/http" "github.com/opencloud-eu/opencloud/pkg/log" - svchttp "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/http" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" token "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" ) -type RedeemService interface { - VerifyToken(tokenString string) (storage.Record, error) -} - +// RedeemRequest is the request body for token redemption. type RedeemRequest struct { Token string `json:"token"` } -func RedeemHandler(log log.Logger, s RedeemService) func(w http.ResponseWriter, r *http.Request) { +// RedeemHandler validates the token submitted to the redeem endpoint. +func RedeemHandler(log log.Logger, s *guestauth.GuestAuthService) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var req RedeemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -31,7 +29,7 @@ func RedeemHandler(log log.Logger, s RedeemService) func(w http.ResponseWriter, _, err := s.VerifyToken(req.Token) if err != nil { switch { - case errors.Is(err, svchttp.ErrExpired) || errors.Is(err, svchttp.ErrAlreadyRedeemed): + case errors.Is(err, guestauth.ErrExpired) || errors.Is(err, guestauth.ErrAlreadyRedeemed): log.Debug().Err(err).Msg("token expired or already redeemed") w.WriteHeader(http.StatusGone) case errors.Is(err, storage.ErrNotFound): diff --git a/services/guestauth/pkg/service/events/handlers.go b/services/guestauth/pkg/service/events/handlers.go new file mode 100644 index 0000000000..f736bea6d2 --- /dev/null +++ b/services/guestauth/pkg/service/events/handlers.go @@ -0,0 +1,55 @@ +package events + +import ( + "context" + "time" + + user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + ocEvents "github.com/opencloud-eu/opencloud/pkg/events" + "github.com/opencloud-eu/reva/v2/pkg/events" +) + +// handleShareCreated handles a share created event. +func (s *EventConsumer) handleShareCreated(ctx context.Context, ev events.ShareCreated) error { + _, span := tracer.Start(ctx, "handleShareCreated") + defer span.End() + + if ev.GranteeUserID == nil || ev.GranteeUserID.GetType() != user.UserType_USER_TYPE_GUEST { + s.log.Debug().Msg("share created event is not for a guest, skipping") + return nil + } + + tok, err := s.guestAuth.CreateToken(ev.ShareID.GetOpaqueId()) + if err != nil { + return err + } + + return events.Publish(ctx, s.stream, ocEvents.GuestTokenCreated{ + ShareID: ev.ShareID, + Sharer: ev.Sharer, + ItemID: ev.ItemID, + ResourceName: ev.ResourceName, + Token: tok.String(), + Timestamp: time.Now(), + }) +} + +// handleShareRemoved handles a share removed event. +func (s *EventConsumer) handleShareRemoved(ctx context.Context, ev events.ShareRemoved) error { + _, span := tracer.Start(ctx, "handleShareRemoved") + defer span.End() + + s.log.Debug().Interface("event", ev).Msg("share removed event received") + + return s.guestAuth.CleanupShare(ev.ShareID.GetOpaqueId()) +} + +// handleShareExpired handles a share expired event. +func (s *EventConsumer) handleShareExpired(ctx context.Context, ev events.ShareExpired) error { + _, span := tracer.Start(ctx, "handleShareExpired") + defer span.End() + + s.log.Debug().Interface("event", ev).Msg("share expired event received") + + return s.guestAuth.CleanupShare(ev.ShareID.GetOpaqueId()) +} diff --git a/services/guestauth/pkg/service/events/handlers_test.go b/services/guestauth/pkg/service/events/handlers_test.go new file mode 100644 index 0000000000..f5ad313420 --- /dev/null +++ b/services/guestauth/pkg/service/events/handlers_test.go @@ -0,0 +1,70 @@ +package events + +import ( + "context" + "testing" + "time" + + collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" + "github.com/opencloud-eu/reva/v2/pkg/events" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testShareID = "e0123456-7890-abcd-ef01-234567890abc" + +func addRecord(t *testing.T, store storage.Storage, shareID string) storage.Record { + ts := token.NewTokenService() + tok, err := ts.Generate(shareID) + require.NoError(t, err) + + rec := storage.Record{ + ShareID: shareID, + ShareIDHash: tok.ShareIDHash, + SecretHash: tok.SecretHash, + Expiry: time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC), + } + require.NoError(t, store.Add(rec)) + + return rec +} + +func newConsumer(t *testing.T) (*EventConsumer, storage.Storage) { + store := storage.NewFileStorage(t.TempDir()) + guestAuth := guestauth.NewGuestAuthService(token.NewTokenService(), store) + consumer, err := NewEventConsumer(nil, GuestAuthService(guestAuth)) + require.NoError(t, err) + + return consumer, store +} + +func TestHandleShareRemoved(t *testing.T) { + svc, store := newConsumer(t) + rec := addRecord(t, store, testShareID) + + ev := events.ShareRemoved{ + ShareID: &collaboration.ShareId{OpaqueId: testShareID}, + } + + require.NoError(t, svc.handleShareRemoved(context.Background(), ev)) + + _, err := store.Get(rec.ShareIDHash) + assert.ErrorIs(t, err, storage.ErrNotFound) +} + +func TestHandleShareExpired(t *testing.T) { + svc, store := newConsumer(t) + rec := addRecord(t, store, testShareID) + + ev := events.ShareExpired{ + ShareID: &collaboration.ShareId{OpaqueId: testShareID}, + } + + require.NoError(t, svc.handleShareExpired(context.Background(), ev)) + + _, err := store.Get(rec.ShareIDHash) + assert.ErrorIs(t, err, storage.ErrNotFound) +} diff --git a/services/guestauth/pkg/service/events/options.go b/services/guestauth/pkg/service/events/options.go index eb95ba1d95..b82e03da0f 100644 --- a/services/guestauth/pkg/service/events/options.go +++ b/services/guestauth/pkg/service/events/options.go @@ -4,6 +4,7 @@ import ( "context" "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" "github.com/opencloud-eu/reva/v2/pkg/events" ) @@ -17,6 +18,7 @@ type Options struct { Stream events.Stream RegisteredEvents []events.Unmarshaller NumConsumers int + GuestAuthService *guestauth.GuestAuthService } // Context configures a context for the guestauth service @@ -53,3 +55,10 @@ func NumConsumers(num int) Option { o.NumConsumers = num } } + +// GuestAuthService configures the guest auth domain service. +func GuestAuthService(s *guestauth.GuestAuthService) Option { + return func(o *Options) { + o.GuestAuthService = s + } +} diff --git a/services/guestauth/pkg/service/events/service.go b/services/guestauth/pkg/service/events/service.go index b399cb85ff..beab55323b 100644 --- a/services/guestauth/pkg/service/events/service.go +++ b/services/guestauth/pkg/service/events/service.go @@ -6,6 +6,7 @@ import ( "sync/atomic" "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" "github.com/opencloud-eu/reva/v2/pkg/events" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -21,12 +22,14 @@ var ( _numConsumersDefault = 1 ) -// GuestauthService consumes events for the guestauth service -type GuestauthService struct { +// EventConsumer consumes guest share events. +type EventConsumer struct { ctx context.Context log log.Logger stream events.Stream + guestAuth *guestauth.GuestAuthService + numConsumers int events []events.Unmarshaller @@ -35,8 +38,8 @@ type GuestauthService struct { stopped *atomic.Bool } -// New creates a new GuestauthService -func New(stream events.Stream, opts ...Option) (*GuestauthService, error) { +// NewEventConsumer creates a new event consumer. +func NewEventConsumer(stream events.Stream, opts ...Option) (*EventConsumer, error) { o := &Options{ NumConsumers: _numConsumersDefault, } @@ -44,10 +47,11 @@ func New(stream events.Stream, opts ...Option) (*GuestauthService, error) { opt(o) } - s := &GuestauthService{ + s := &EventConsumer{ ctx: o.Context, log: o.Logger, stream: stream, + guestAuth: o.GuestAuthService, events: o.RegisteredEvents, numConsumers: o.NumConsumers, stopCh: make(chan struct{}, 1), @@ -58,7 +62,7 @@ func New(stream events.Stream, opts ...Option) (*GuestauthService, error) { } // Run to fulfil Runner interface -func (s *GuestauthService) Run() error { +func (s *EventConsumer) Run() error { ch, err := events.Consume(s.stream, "guestauth", s.events...) if err != nil { return err @@ -108,14 +112,14 @@ func (s *GuestauthService) Run() error { // Close will make the service to stop processing, so the `Run` // method can finish. -func (s *GuestauthService) Close() { +func (s *EventConsumer) Close() { if s.stopped.CompareAndSwap(false, true) { close(s.stopCh) } } // processEvent dispatches an event to the matching handler. -func (s *GuestauthService) processEvent(e events.Event) error { +func (s *EventConsumer) processEvent(e events.Event) error { ctx := e.GetTraceContext(s.ctx) ctx, span := tracer.Start(ctx, "processEvent") defer span.End() @@ -137,33 +141,3 @@ func (s *GuestauthService) processEvent(e events.Event) error { return nil } - -// handleShareCreated handles a share created event. -func (s *GuestauthService) handleShareCreated(ctx context.Context, ev events.ShareCreated) error { - _, span := tracer.Start(ctx, "handleShareCreated") - defer span.End() - - s.log.Debug().Interface("event", ev).Msg("share created event received") - - return nil -} - -// handleShareRemoved handles a share removed event. -func (s *GuestauthService) handleShareRemoved(ctx context.Context, ev events.ShareRemoved) error { - _, span := tracer.Start(ctx, "handleShareRemoved") - defer span.End() - - s.log.Debug().Interface("event", ev).Msg("share removed event received") - - return nil -} - -// handleShareExpired handles a share expired event. -func (s *GuestauthService) handleShareExpired(ctx context.Context, ev events.ShareExpired) error { - _, span := tracer.Start(ctx, "handleShareExpired") - defer span.End() - - s.log.Debug().Interface("event", ev).Msg("share expired event received") - - return nil -} diff --git a/services/guestauth/pkg/service/guestauth/service.go b/services/guestauth/pkg/service/guestauth/service.go new file mode 100644 index 0000000000..3bd1916f71 --- /dev/null +++ b/services/guestauth/pkg/service/guestauth/service.go @@ -0,0 +1,82 @@ +package guestauth + +import ( + "errors" + "time" + + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" +) + +var ErrExpired = errors.New("token expired") +var ErrAlreadyRedeemed = errors.New("token already redeemed") + +// GuestAuthService contains the business logic shared by guestauth transport services. +type GuestAuthService struct { + tokenSvc *token.TokenService + store storage.Storage +} + +func NewGuestAuthService(tokenSvc *token.TokenService, store storage.Storage) *GuestAuthService { + return &GuestAuthService{ + tokenSvc: tokenSvc, + store: store, + } +} + +func (s *GuestAuthService) CreateToken(shareID string) (*token.Token, error) { + tok, err := s.tokenSvc.Generate(shareID) + if err != nil { + return nil, err + } + + // ShareCreated carries no expiration; expiry is checked against the share when the token is redeemed. + if err := s.store.Add(storage.Record{ + ShareID: shareID, + ShareIDHash: tok.ShareIDHash, + SecretHash: tok.SecretHash, + Redeemed: false, + }); err != nil { + return nil, err + } + + return tok, nil +} + +// VerifyToken validates a token and returns its stored record. +func (s *GuestAuthService) VerifyToken(tokenString string) (storage.Record, error) { + tok, err := s.tokenSvc.Parse(tokenString) + if err != nil { + return storage.Record{}, err + } + + rec, err := s.store.Get(tok.ShareIDHash) + if err != nil { + return storage.Record{}, err + } + + if err := s.tokenSvc.Verify(*tok, rec.SecretHash); err != nil { + return storage.Record{}, err + } + + if !rec.Expiry.IsZero() && rec.Expiry.Before(time.Now()) { + return storage.Record{}, ErrExpired + } + + if rec.Redeemed { + return storage.Record{}, ErrAlreadyRedeemed + } + + return rec, nil +} + +// CleanupShare removes a share's token record from storage. Missing records are ignored. +func (s *GuestAuthService) CleanupShare(shareID string) error { + shareIDHash := token.Hash(shareID) + err := s.store.Remove(shareIDHash) + if err != nil && err != storage.ErrNotFound { + return err + } + + return nil +} diff --git a/services/guestauth/pkg/service/http/service_test.go b/services/guestauth/pkg/service/guestauth/service_test.go similarity index 66% rename from services/guestauth/pkg/service/http/service_test.go rename to services/guestauth/pkg/service/guestauth/service_test.go index 15c6125d78..f110fd2f5a 100644 --- a/services/guestauth/pkg/service/http/service_test.go +++ b/services/guestauth/pkg/service/guestauth/service_test.go @@ -1,7 +1,6 @@ -package http +package guestauth import ( - "strings" "testing" "time" @@ -17,32 +16,24 @@ func newToken(t *testing.T) (string, storage.Record) { ts := token.NewTokenService() tok, err := ts.Generate(testShareID) require.NoError(t, err) - parts := strings.Split(tok, ".") rec := storage.Record{ ShareID: testShareID, - ShareIDHash: parts[1], - SecretHash: ts.Hash(parts[2]), + ShareIDHash: tok.ShareIDHash, + SecretHash: tok.SecretHash, Expiry: time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC), } - return tok, rec + return tok.String(), rec } func newStorage(t *testing.T) storage.Storage { return storage.NewFileStorage(t.TempDir()) } -func newService(t *testing.T, store storage.Storage) *svc { - s, err := NewService(token.NewTokenService(), store) - require.NoError(t, err) - - return s -} - func TestVerifyTokenValid(t *testing.T) { store := newStorage(t) - s := newService(t, store) + s := NewGuestAuthService(token.NewTokenService(), store) tok, rec := newToken(t) require.NoError(t, store.Add(rec)) @@ -51,9 +42,25 @@ func TestVerifyTokenValid(t *testing.T) { assert.Equal(t, rec, got) } +func TestCreateTokenPersistsRecord(t *testing.T) { + store := newStorage(t) + s := NewGuestAuthService(token.NewTokenService(), store) + + tok, err := s.CreateToken(testShareID) + require.NoError(t, err) + + rec, err := store.Get(tok.ShareIDHash) + require.NoError(t, err) + assert.Equal(t, testShareID, rec.ShareID) + assert.Equal(t, tok.ShareIDHash, rec.ShareIDHash) + assert.Equal(t, tok.SecretHash, rec.SecretHash) + assert.True(t, rec.Expiry.IsZero()) + assert.False(t, rec.Redeemed) +} + func TestVerifyTokenExpired(t *testing.T) { store := newStorage(t) - s := newService(t, store) + s := NewGuestAuthService(token.NewTokenService(), store) tok, rec := newToken(t) rec.Expiry = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) require.NoError(t, store.Add(rec)) @@ -64,7 +71,7 @@ func TestVerifyTokenExpired(t *testing.T) { func TestVerifyTokenAlreadyRedeemed(t *testing.T) { store := newStorage(t) - s := newService(t, store) + s := NewGuestAuthService(token.NewTokenService(), store) tok, rec := newToken(t) require.NoError(t, store.Add(rec)) require.NoError(t, store.Redeem(rec.ShareIDHash)) diff --git a/services/guestauth/pkg/service/http/option.go b/services/guestauth/pkg/service/http/option.go deleted file mode 100644 index 8eb32916d7..0000000000 --- a/services/guestauth/pkg/service/http/option.go +++ /dev/null @@ -1,31 +0,0 @@ -package http - -import ( - "github.com/opencloud-eu/opencloud/pkg/log" -) - -// Option defines a single option function. -type Option func(o *Options) - -// Options defines the available options for this package. -type Options struct { - Logger log.Logger -} - -// newOptions initializes the available default options. -func newOptions(opts ...Option) Options { - opt := Options{} - - for _, o := range opts { - o(&opt) - } - - return opt -} - -// Logger provides a function to set the logger option. -func Logger(val log.Logger) Option { - return func(o *Options) { - o.Logger = val - } -} diff --git a/services/guestauth/pkg/service/http/service.go b/services/guestauth/pkg/service/http/service.go deleted file mode 100644 index 34592070d7..0000000000 --- a/services/guestauth/pkg/service/http/service.go +++ /dev/null @@ -1,56 +0,0 @@ -package http - -import ( - "errors" - "time" - - "github.com/opencloud-eu/opencloud/pkg/log" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" -) - -var ErrExpired = errors.New("token expired") -var ErrAlreadyRedeemed = errors.New("token already redeemed") - -func NewService(tokenSvc *token.TokenService, store storage.Storage, opts ...Option) (*svc, error) { - o := newOptions(opts...) - - return &svc{ - log: o.Logger, - tokenSvc: tokenSvc, - store: store, - }, nil -} - -// svc provides the logic behind the http endpoints of the guestauth service. -type svc struct { - log log.Logger - tokenSvc *token.TokenService - store storage.Storage -} - -func (s *svc) VerifyToken(tokenString string) (storage.Record, error) { - shareIDHash, err := s.tokenSvc.ShareIDHash(tokenString) - if err != nil { - return storage.Record{}, err - } - - rec, err := s.store.Get(shareIDHash) - if err != nil { - return storage.Record{}, err - } - - if err := s.tokenSvc.Verify(tokenString, rec.SecretHash); err != nil { - return storage.Record{}, err - } - - if !rec.Expiry.IsZero() && rec.Expiry.Before(time.Now()) { - return storage.Record{}, ErrExpired - } - - if rec.Redeemed { - return storage.Record{}, ErrAlreadyRedeemed - } - - return rec, nil -} diff --git a/services/guestauth/pkg/service/storage/file_storage_test.go b/services/guestauth/pkg/service/storage/file_storage_test.go index 1ba349adba..1f9b9e549b 100644 --- a/services/guestauth/pkg/service/storage/file_storage_test.go +++ b/services/guestauth/pkg/service/storage/file_storage_test.go @@ -1,7 +1,6 @@ package storage import ( - "strings" "testing" "time" @@ -16,8 +15,8 @@ func newRecord(shareID string) Record { return Record{ ShareID: shareID, - ShareIDHash: strings.Split(tok, ".")[1], - SecretHash: svc.Hash(strings.Split(tok, ".")[2]), + ShareIDHash: tok.ShareIDHash, + SecretHash: tok.SecretHash, Expiry: time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC), } } diff --git a/services/guestauth/pkg/service/token/token.go b/services/guestauth/pkg/service/token/token.go index fac4467aa2..5c42431a51 100644 --- a/services/guestauth/pkg/service/token/token.go +++ b/services/guestauth/pkg/service/token/token.go @@ -17,48 +17,59 @@ const ( var ErrInvalidToken = errors.New("invalid token") +type Token struct { + ShareIDHash string + SecretHash string + secret string +} + +func (t *Token) String() string { + return strings.Join([]string{tokenVersion, t.ShareIDHash, t.secret}, ".") +} + type TokenService struct{} func NewTokenService() *TokenService { return &TokenService{} } -func (s *TokenService) Generate(shareID string) (string, error) { +func (s *TokenService) Generate(shareID string) (*Token, error) { secretBytes := make([]byte, secretLength) if _, err := rand.Read(secretBytes); err != nil { - return "", fmt.Errorf("could not generate random secret: %w", err) + return nil, fmt.Errorf("could not generate random secret: %w", err) } - return strings.Join([]string{ - tokenVersion, - s.Hash(shareID), - base64.RawURLEncoding.EncodeToString(secretBytes), - }, "."), nil + secret := base64.RawURLEncoding.EncodeToString(secretBytes) + return &Token{ + ShareIDHash: Hash(shareID), + SecretHash: Hash(secret), + secret: secret, + }, nil } -func (s *TokenService) Verify(tokenString string, storedSecretHash string) error { - parts := strings.Split(tokenString, ".") +func (s *TokenService) Parse(encoded string) (*Token, error) { + parts := strings.Split(encoded, ".") if len(parts) != tokenParts || parts[0] != tokenVersion || parts[1] == "" || parts[2] == "" { - return ErrInvalidToken + return nil, ErrInvalidToken } - if s.Hash(parts[2]) != storedSecretHash { + return &Token{ + ShareIDHash: parts[1], + SecretHash: Hash(parts[2]), + secret: parts[2], + }, nil +} + +func (s *TokenService) Verify(candidate Token, storedSecretHash string) error { + secretHash := Hash(candidate.secret) + if candidate.ShareIDHash == "" || candidate.secret == "" || candidate.SecretHash != secretHash || secretHash != storedSecretHash { return ErrInvalidToken } return nil } -func (s *TokenService) ShareIDHash(tokenString string) (string, error) { - parts := strings.Split(tokenString, ".") - if len(parts) != tokenParts || parts[0] != tokenVersion || parts[1] == "" || parts[2] == "" { - return "", ErrInvalidToken - } - - return parts[1], nil -} - -func (s *TokenService) Hash(str string) string { - h := sha256.Sum256([]byte(str)) +func Hash(value string) string { + h := sha256.Sum256([]byte(value)) return base64.RawURLEncoding.EncodeToString(h[:]) } diff --git a/services/guestauth/pkg/service/token/token_test.go b/services/guestauth/pkg/service/token/token_test.go index 6cb5aff09c..8f5c07f96b 100644 --- a/services/guestauth/pkg/service/token/token_test.go +++ b/services/guestauth/pkg/service/token/token_test.go @@ -1,7 +1,6 @@ package token import ( - "strings" "testing" "github.com/stretchr/testify/assert" @@ -10,20 +9,17 @@ import ( const testShareID = "e0123456-7890-abcd-ef01-234567890abc" -func TestGenerate(t *testing.T) { +func TestGenerateAndString(t *testing.T) { svc := NewTokenService() tok, err := svc.Generate(testShareID) require.NoError(t, err) - - parts := strings.Split(tok, ".") - require.Len(t, parts, tokenParts) - assert.Equal(t, tokenVersion, parts[0]) - assert.Equal(t, svc.Hash(testShareID), parts[1]) - assert.NotEmpty(t, parts[2]) + assert.Equal(t, Hash(testShareID), tok.ShareIDHash) + assert.NotEmpty(t, tok.SecretHash) + assert.NotEmpty(t, tok.String()) } -func TestGenerateDeterminism(t *testing.T) { +func TestGenerateRandomizesSecret(t *testing.T) { svc := NewTokenService() tok1, err := svc.Generate(testShareID) @@ -31,88 +27,73 @@ func TestGenerateDeterminism(t *testing.T) { tok2, err := svc.Generate(testShareID) require.NoError(t, err) - assert.Equal(t, svc.Hash(testShareID), strings.Split(tok1, ".")[1]) - assert.Equal(t, svc.Hash(testShareID), strings.Split(tok2, ".")[1]) - assert.NotEqual(t, tok1, tok2) + assert.Equal(t, tok1.ShareIDHash, tok2.ShareIDHash) + assert.NotEqual(t, tok1.SecretHash, tok2.SecretHash) + assert.NotEqual(t, tok1.String(), tok2.String()) other, err := svc.Generate("9f9f9f9-9f9f-9f9f-9f9f-9f9f9f9f9f9f") require.NoError(t, err) - assert.NotEqual(t, strings.Split(tok1, ".")[1], strings.Split(other, ".")[1]) + assert.NotEqual(t, tok1.ShareIDHash, other.ShareIDHash) +} + +func TestParse(t *testing.T) { + svc := NewTokenService() + original, err := svc.Generate(testShareID) + require.NoError(t, err) + + tests := []struct { + name string + encoded string + wantErr bool + }{ + {name: "valid", encoded: original.String()}, + {name: "wrong version", encoded: "v2." + original.ShareIDHash + "." + original.secret, wantErr: true}, + {name: "missing version", encoded: original.ShareIDHash + "." + original.secret, wantErr: true}, + {name: "too many parts", encoded: original.String() + ".extra", wantErr: true}, + {name: "empty share hash", encoded: "v1.." + original.secret, wantErr: true}, + {name: "empty secret", encoded: "v1." + original.ShareIDHash + ".", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parsed, err := svc.Parse(tt.encoded) + if tt.wantErr { + assert.ErrorIs(t, err, ErrInvalidToken) + return + } + + require.NoError(t, err) + assert.Equal(t, original.ShareIDHash, parsed.ShareIDHash) + assert.Equal(t, original.SecretHash, parsed.SecretHash) + assert.Equal(t, original.String(), parsed.String()) + }) + } } func TestVerify(t *testing.T) { svc := NewTokenService() - tok, err := svc.Generate(testShareID) require.NoError(t, err) - hashPart := strings.Split(tok, ".")[1] - secretPart := strings.Split(tok, ".")[2] - storedSecretHash := svc.Hash(secretPart) - tests := []struct { name string - token string + token Token storedSecretHash string - expectError bool + wantErr bool }{ - { - name: "valid token", - token: tok, - storedSecretHash: storedSecretHash, - }, - { - name: "tampered secret", - token: "v1." + hashPart + ".tampered", - storedSecretHash: storedSecretHash, - expectError: true, - }, - { - name: "wrong stored secret", - token: tok, - storedSecretHash: svc.Hash("other-secret"), - expectError: true, - }, - { - name: "wrong version", - token: "v2." + hashPart + "." + secretPart, - storedSecretHash: storedSecretHash, - expectError: true, - }, - { - name: "missing version", - token: hashPart + "." + secretPart, - storedSecretHash: storedSecretHash, - expectError: true, - }, - { - name: "too many parts", - token: "v1." + hashPart + "." + secretPart + ".extra", - storedSecretHash: storedSecretHash, - expectError: true, - }, - { - name: "empty hash", - token: "v1.." + secretPart, - storedSecretHash: storedSecretHash, - expectError: true, - }, - { - name: "empty secret", - token: "v1." + hashPart + ".", - storedSecretHash: storedSecretHash, - expectError: true, - }, + {name: "valid", token: *tok, storedSecretHash: tok.SecretHash}, + {name: "wrong stored secret", token: *tok, storedSecretHash: Hash("other-secret"), wantErr: true}, + {name: "missing fields", token: Token{ShareIDHash: tok.ShareIDHash}, storedSecretHash: tok.SecretHash, wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := svc.Verify(tt.token, tt.storedSecretHash) - if tt.expectError { + if tt.wantErr { assert.ErrorIs(t, err, ErrInvalidToken) - } else { - assert.NoError(t, err) + return } + assert.NoError(t, err) }) } } From ecb546fe469fae0de4b6e3b87e95e26cd0671772 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Fri, 25 Sep 2026 10:12:50 +0200 Subject: [PATCH 07/32] feat(guestauth): create jwt token and setup cookies on redeem token call --- services/guestauth/pkg/command/server.go | 32 +++- services/guestauth/pkg/config/config.go | 11 +- .../pkg/config/defaults/defaultconfig.go | 12 +- services/guestauth/pkg/config/parser/parse.go | 4 + services/guestauth/pkg/server/http/redeem.go | 33 +++- .../guestauth/pkg/server/http/redeem_test.go | 112 +++++++++++ services/guestauth/pkg/server/http/server.go | 2 +- .../pkg/service/guestauth/options.go | 38 ++++ .../pkg/service/guestauth/service.go | 117 ++++++++++-- .../pkg/service/guestauth/service_test.go | 177 +++++++++++++++--- services/guestauth/pkg/service/jwt/jwt.go | 34 ++++ .../guestauth/pkg/service/jwt/jwt_test.go | 41 ++++ 12 files changed, 564 insertions(+), 49 deletions(-) create mode 100644 services/guestauth/pkg/server/http/redeem_test.go create mode 100644 services/guestauth/pkg/service/guestauth/options.go create mode 100644 services/guestauth/pkg/service/jwt/jwt.go create mode 100644 services/guestauth/pkg/service/jwt/jwt_test.go diff --git a/services/guestauth/pkg/command/server.go b/services/guestauth/pkg/command/server.go index 8d4abd82c6..1df914c706 100644 --- a/services/guestauth/pkg/command/server.go +++ b/services/guestauth/pkg/command/server.go @@ -9,7 +9,9 @@ import ( "github.com/opencloud-eu/opencloud/pkg/config/configlog" "github.com/opencloud-eu/opencloud/pkg/generators" "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/pkg/registry" "github.com/opencloud-eu/opencloud/pkg/runner" + "github.com/opencloud-eu/opencloud/pkg/tracing" "github.com/opencloud-eu/opencloud/pkg/version" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config/parser" @@ -18,10 +20,12 @@ import ( "github.com/opencloud-eu/opencloud/services/guestauth/pkg/server/http" svcEvents "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/events" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/jwt" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" "github.com/opencloud-eu/reva/v2/pkg/events" "github.com/opencloud-eu/reva/v2/pkg/events/stream" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" ) var _registeredEvents = []events.Unmarshaller{ @@ -41,6 +45,26 @@ func Server(cfg *config.Config) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel) + tracerProvider, err := tracing.GetTraceProvider(cmd.Context(), cfg.Commons.TracesExporter, cfg.Service.Name) + if err != nil { + return err + } + + tm, err := pool.StringToTLSMode(cfg.GRPCClientTLS.Mode) + if err != nil { + return err + } + gatewaySelector, err := pool.GatewaySelector( + cfg.RevaGateway, + pool.WithTLSCACert(cfg.GRPCClientTLS.CACert), + pool.WithTLSMode(tm), + pool.WithRegistry(registry.GetRegistry()), + pool.WithTracerProvider(tracerProvider), + ) + if err != nil { + return fmt.Errorf("could not get reva client selector: %s", err) + } + gr := runner.NewGroup() ctx, cancel := context.WithCancel(cmd.Context()) defer cancel() @@ -50,7 +74,13 @@ func Server(cfg *config.Config) *cobra.Command { tokenSvc := token.NewTokenService() store := storage.NewFileStorage(cfg.Storage.RootDirectory) - guestAuth := guestauth.NewGuestAuthService(tokenSvc, store) + jwtService := jwt.NewJwtService(cfg.TokenManager.JWTSecret, cfg.JWT.TTL) + + guestAuth := guestauth.NewGuestAuthService(tokenSvc, store, + guestauth.GatewaySelector(gatewaySelector), + guestauth.ServiceAccount(cfg.ServiceAccount), + guestauth.JWT(jwtService), + ) if !cfg.HTTP.Disabled { server, err := http.Server( diff --git a/services/guestauth/pkg/config/config.go b/services/guestauth/pkg/config/config.go index aa503f2738..2136394977 100644 --- a/services/guestauth/pkg/config/config.go +++ b/services/guestauth/pkg/config/config.go @@ -2,6 +2,7 @@ package config import ( "context" + "time" "github.com/opencloud-eu/opencloud/pkg/shared" ) @@ -24,6 +25,7 @@ type Config struct { HTTP HTTP `yaml:"http"` Storage Storage `yaml:"storage"` TokenManager *TokenManager `yaml:"token_manager"` + JWT JWT `yaml:"jwt"` ServiceAccount ServiceAccount `yaml:"service_account"` @@ -75,5 +77,12 @@ type Storage struct { // TokenManager is the config for using the reva token manager type TokenManager struct { - JWTSecret string `yaml:"jwt_secret" env:"OC_JWT_SECRET;GUESTAUTH_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"1.0.0"` + JWTSecret string `yaml:"jwt_secret" env:"GUESTAUTH_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"1.0.0"` +} + +// JWT defines the configuration for guest session tokens. +type JWT struct { + CookieName string `yaml:"cookie_name" env:"GUESTAUTH_JWT_COOKIE_NAME" desc:"The name of the session cookie set when a guest token is redeemed." introductionVersion:"1.0.0"` + CookieSecure bool `yaml:"cookie_secure" env:"GUESTAUTH_JWT_COOKIE_SECURE" desc:"Whether the session cookie should be flagged as secure (only sent over HTTPS)." introductionVersion:"1.0.0"` + TTL time.Duration `yaml:"ttl" env:"GUESTAUTH_JWT_TTL" desc:"The lifetime of a redeemed guest session token." introductionVersion:"1.0.0"` } diff --git a/services/guestauth/pkg/config/defaults/defaultconfig.go b/services/guestauth/pkg/config/defaults/defaultconfig.go index 80ebf45869..2d217bb298 100644 --- a/services/guestauth/pkg/config/defaults/defaultconfig.go +++ b/services/guestauth/pkg/config/defaults/defaultconfig.go @@ -2,6 +2,7 @@ package defaults import ( "path" + "time" "github.com/opencloud-eu/opencloud/pkg/config/defaults" "github.com/opencloud-eu/opencloud/pkg/shared" @@ -50,6 +51,11 @@ func DefaultConfig() *config.Config { Storage: config.Storage{ RootDirectory: path.Join(defaults.BaseDataPath(), "guestauth"), }, + JWT: config.JWT{ + CookieName: "oc_guest_session", + CookieSecure: true, + TTL: 24 * time.Hour, + }, } } @@ -62,11 +68,7 @@ func EnsureDefaults(cfg *config.Config) { cfg.GRPCClientTLS = structs.CopyOrZeroValue(cfg.Commons.GRPCClientTLS) } - if cfg.TokenManager == nil && cfg.Commons != nil && cfg.Commons.TokenManager != nil { - cfg.TokenManager = &config.TokenManager{ - JWTSecret: cfg.Commons.TokenManager.JWTSecret, - } - } else if cfg.TokenManager == nil { + if cfg.TokenManager == nil { cfg.TokenManager = &config.TokenManager{} } diff --git a/services/guestauth/pkg/config/parser/parse.go b/services/guestauth/pkg/config/parser/parse.go index 4702330dfc..ff036e858f 100644 --- a/services/guestauth/pkg/config/parser/parse.go +++ b/services/guestauth/pkg/config/parser/parse.go @@ -4,6 +4,7 @@ import ( "errors" occfg "github.com/opencloud-eu/opencloud/pkg/config" + "github.com/opencloud-eu/opencloud/pkg/shared" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config/defaults" @@ -34,5 +35,8 @@ func ParseConfig(cfg *config.Config) error { // Validate validates the config func Validate(cfg *config.Config) error { + if cfg.TokenManager == nil || cfg.TokenManager.JWTSecret == "" { + return shared.MissingJWTTokenError(cfg.Service.Name) + } return nil } diff --git a/services/guestauth/pkg/server/http/redeem.go b/services/guestauth/pkg/server/http/redeem.go index 4e632f2186..e3c386a806 100644 --- a/services/guestauth/pkg/server/http/redeem.go +++ b/services/guestauth/pkg/server/http/redeem.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" token "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" @@ -17,7 +18,7 @@ type RedeemRequest struct { } // RedeemHandler validates the token submitted to the redeem endpoint. -func RedeemHandler(log log.Logger, s *guestauth.GuestAuthService) func(w http.ResponseWriter, r *http.Request) { +func RedeemHandler(log log.Logger, s *guestauth.GuestAuthService, cfg *config.Config) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var req RedeemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -26,26 +27,44 @@ func RedeemHandler(log log.Logger, s *guestauth.GuestAuthService) func(w http.Re return } - _, err := s.VerifyToken(req.Token) + sessionToken, err := s.Redeem(r.Context(), req.Token) if err != nil { switch { - case errors.Is(err, guestauth.ErrExpired) || errors.Is(err, guestauth.ErrAlreadyRedeemed): - log.Debug().Err(err).Msg("token expired or already redeemed") + case errors.Is(err, guestauth.ErrAlreadyRedeemed): + log.Debug().Err(err).Msg("token already redeemed") + w.WriteHeader(http.StatusConflict) + case errors.Is(err, guestauth.ErrExpired): + log.Debug().Err(err).Msg("token expired") w.WriteHeader(http.StatusGone) case errors.Is(err, storage.ErrNotFound): - log.Debug().Err(err).Msg("no token record found") + log.Debug().Err(err).Msg("token not found") w.WriteHeader(http.StatusNotFound) case errors.Is(err, token.ErrInvalidToken): log.Debug().Err(err).Msg("token is invalid") w.WriteHeader(http.StatusUnauthorized) + case errors.Is(err, guestauth.ErrShareNotFound): + log.Debug().Err(err).Msg("share not found") + w.WriteHeader(http.StatusNotFound) + case errors.Is(err, guestauth.ErrShareExpired): + log.Debug().Err(err).Msg("share expired") + w.WriteHeader(http.StatusGone) + default: - log.Error().Err(err).Msg("error verifying token") + log.Error().Err(err).Msg("error redeeming token") w.WriteHeader(http.StatusInternalServerError) } return } - // session create should be here + http.SetCookie(w, &http.Cookie{ + Name: cfg.JWT.CookieName, + Value: sessionToken, + Path: "/", + HttpOnly: true, + Secure: cfg.JWT.CookieSecure, + SameSite: http.SameSiteLaxMode, + MaxAge: int(cfg.JWT.TTL.Seconds()), + }) w.WriteHeader(http.StatusOK) } } diff --git a/services/guestauth/pkg/server/http/redeem_test.go b/services/guestauth/pkg/server/http/redeem_test.go new file mode 100644 index 0000000000..17b4c13597 --- /dev/null +++ b/services/guestauth/pkg/server/http/redeem_test.go @@ -0,0 +1,112 @@ +package http + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/jwt" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +const testShareID = "e0123456-7890-abcd-ef01-234567890abc" + +type gatewayTestSelector struct { + client gateway.GatewayAPIClient +} + +func (s gatewayTestSelector) Next(...pool.Option) (gateway.GatewayAPIClient, error) { + return s.client, nil +} + +func newGatewayMock() *cs3mocks.GatewayAPIClient { + gwc := &cs3mocks.GatewayAPIClient{} + gwc.On("Authenticate", mock.Anything, mock.Anything). + Return(&gateway.AuthenticateResponse{ + Status: &rpc.Status{Code: rpc.Code_CODE_OK}, + Token: "token", + }, nil) + gwc.On("GetShare", mock.Anything, mock.Anything).Return(&collaboration.GetShareResponse{ + Status: &rpc.Status{Code: rpc.Code_CODE_OK}, + Share: &collaboration.Share{ + Id: &collaboration.ShareId{OpaqueId: testShareID}, + }, + }, nil) + return gwc +} + +func newRedeemHandler(t *testing.T, store storage.Storage) http.HandlerFunc { + t.Helper() + svc := guestauth.NewGuestAuthService( + token.NewTokenService(), + store, + guestauth.GatewaySelector(gatewayTestSelector{client: newGatewayMock()}), + guestauth.ServiceAccount(config.ServiceAccount{ServiceAccountID: "sa-id", ServiceAccountSecret: "sa-secret"}), + guestauth.JWT(jwt.NewJwtService("test-secret", time.Hour)), + ) + cfg := &config.Config{ + JWT: config.JWT{ + CookieName: "oc_guest_session", + CookieSecure: true, + TTL: time.Hour, + }, + } + return RedeemHandler(log.NopLogger(), svc, cfg) +} + +func TestRedeemHandler(t *testing.T) { + store := storage.NewFileStorage(t.TempDir()) + ts := token.NewTokenService() + tok, err := ts.Generate(testShareID) + require.NoError(t, err) + require.NoError(t, store.Add(storage.Record{ + ShareID: testShareID, + ShareIDHash: tok.ShareIDHash, + SecretHash: tok.SecretHash, + })) + + body, err := json.Marshal(RedeemRequest{Token: tok.String()}) + require.NoError(t, err) + + rr := httptest.NewRecorder() + newRedeemHandler(t, store)(rr, httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(body)))) + + assert.Equal(t, http.StatusOK, rr.Code) + + var cookie *http.Cookie + for _, c := range rr.Result().Cookies() { + if c.Name == "oc_guest_session" { + cookie = c + } + } + require.NotNil(t, cookie) + assert.True(t, cookie.HttpOnly) + assert.Equal(t, "/", cookie.Path) + assert.NotEmpty(t, cookie.Value) +} + +func TestRedeemHandlerInvalidToken(t *testing.T) { + store := storage.NewFileStorage(t.TempDir()) + body, err := json.Marshal(RedeemRequest{Token: "not-a-valid-token"}) + require.NoError(t, err) + + rr := httptest.NewRecorder() + newRedeemHandler(t, store)(rr, httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(body)))) + + assert.Equal(t, http.StatusUnauthorized, rr.Code) +} diff --git a/services/guestauth/pkg/server/http/server.go b/services/guestauth/pkg/server/http/server.go index a6cb3ac543..468e6ee2fb 100644 --- a/services/guestauth/pkg/server/http/server.go +++ b/services/guestauth/pkg/server/http/server.go @@ -56,7 +56,7 @@ func Server(opts ...Option) (ohttp.Service, error) { mux.Use(middlewares...) mux.Route(options.Config.HTTP.Root, func(r chi.Router) { - r.Post("/v1beta1/guestInvitations/redeem", RedeemHandler(options.Logger, options.Service)) + r.Post("/v1beta1/guestInvitations/redeem", RedeemHandler(options.Logger, options.Service, options.Config)) }) err = micro.RegisterHandler(newService.Server(), mux) diff --git a/services/guestauth/pkg/service/guestauth/options.go b/services/guestauth/pkg/service/guestauth/options.go new file mode 100644 index 0000000000..ca17225a2f --- /dev/null +++ b/services/guestauth/pkg/service/guestauth/options.go @@ -0,0 +1,38 @@ +package guestauth + +import ( + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/jwt" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" +) + +type Option func(*Options) + +// Options for the guestauth service +type Options struct { + GatewaySelector pool.Selectable[gateway.GatewayAPIClient] + ServiceAccount config.ServiceAccount + JWT *jwt.JwtService +} + +// GatewaySelector adds a grpc client selector for the gateway service +func GatewaySelector(gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) Option { + return func(o *Options) { + o.GatewaySelector = gatewaySelector + } +} + +// ServiceAccount configures a service account for the guestauth service +func ServiceAccount(sa config.ServiceAccount) Option { + return func(o *Options) { + o.ServiceAccount = sa + } +} + +// JWT configures the jwt service for the guestauth service +func JWT(m *jwt.JwtService) Option { + return func(o *Options) { + o.JWT = m + } +} diff --git a/services/guestauth/pkg/service/guestauth/service.go b/services/guestauth/pkg/service/guestauth/service.go index 3bd1916f71..5aeba5f52f 100644 --- a/services/guestauth/pkg/service/guestauth/service.go +++ b/services/guestauth/pkg/service/guestauth/service.go @@ -1,26 +1,49 @@ package guestauth import ( + "context" "errors" + "fmt" "time" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/jwt" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + "github.com/opencloud-eu/reva/v2/pkg/utils" ) var ErrExpired = errors.New("token expired") var ErrAlreadyRedeemed = errors.New("token already redeemed") +var ErrShareNotFound = errors.New("share not found") +var ErrShareExpired = errors.New("share expired") // GuestAuthService contains the business logic shared by guestauth transport services. type GuestAuthService struct { - tokenSvc *token.TokenService - store storage.Storage + tokenSvc *token.TokenService + store storage.Storage + gatewaySelector pool.Selectable[gateway.GatewayAPIClient] + serviceAccount config.ServiceAccount + jwtService *jwt.JwtService } -func NewGuestAuthService(tokenSvc *token.TokenService, store storage.Storage) *GuestAuthService { +func NewGuestAuthService(tokenSvc *token.TokenService, store storage.Storage, opts ...Option) *GuestAuthService { + o := &Options{} + for _, opt := range opts { + opt(o) + } + return &GuestAuthService{ - tokenSvc: tokenSvc, - store: store, + tokenSvc: tokenSvc, + store: store, + gatewaySelector: o.GatewaySelector, + serviceAccount: o.ServiceAccount, + jwtService: o.JWT, } } @@ -43,8 +66,42 @@ func (s *GuestAuthService) CreateToken(shareID string) (*token.Token, error) { return tok, nil } +// Redeem validates a token and its share and exchanges them for a session token. +func (s *GuestAuthService) Redeem(ctx context.Context, tokenString string) (string, error) { + rec, err := s.verifyToken(tokenString) + if err != nil { + return "", err + } + + if _, err := s.validateShare(ctx, rec.ShareID); err != nil { + return "", err + } + + sessionToken, err := s.jwtService.Sign(rec.ShareID) + if err != nil { + return "", err + } + + if err := s.store.Redeem(rec.ShareIDHash); err != nil { + return "", err + } + + return sessionToken, nil +} + +// CleanupShare removes a share's token record from storage. Missing records are ignored. +func (s *GuestAuthService) CleanupShare(shareID string) error { + shareIDHash := token.Hash(shareID) + err := s.store.Remove(shareIDHash) + if err != nil && err != storage.ErrNotFound { + return err + } + + return nil +} + // VerifyToken validates a token and returns its stored record. -func (s *GuestAuthService) VerifyToken(tokenString string) (storage.Record, error) { +func (s *GuestAuthService) verifyToken(tokenString string) (storage.Record, error) { tok, err := s.tokenSvc.Parse(tokenString) if err != nil { return storage.Record{}, err @@ -70,13 +127,47 @@ func (s *GuestAuthService) VerifyToken(tokenString string) (storage.Record, erro return rec, nil } -// CleanupShare removes a share's token record from storage. Missing records are ignored. -func (s *GuestAuthService) CleanupShare(shareID string) error { - shareIDHash := token.Hash(shareID) - err := s.store.Remove(shareIDHash) - if err != nil && err != storage.ErrNotFound { - return err +// validateShare extracts the share information from the gateway and checks its existence and expiration. +func (s *GuestAuthService) validateShare(ctx context.Context, shareID string) (*collaboration.Share, error) { + gwc, err := s.gatewaySelector.Next() + if err != nil { + return nil, err } - return nil + ctx, err = utils.GetServiceUserContextWithContext(ctx, gwc, s.serviceAccount.ServiceAccountID, s.serviceAccount.ServiceAccountSecret) + if err != nil { + return nil, err + } + + resp, err := gwc.GetShare(ctx, &collaboration.GetShareRequest{ + Ref: &collaboration.ShareReference{ + Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{ + OpaqueId: shareID, + }, + }, + }, + }) + if err != nil { + return nil, err + } + + switch resp.GetStatus().GetCode() { + case rpc.Code_CODE_OK: + case rpc.Code_CODE_NOT_FOUND: + return nil, ErrShareNotFound + default: + return nil, fmt.Errorf("could not get share %s: %s", shareID, resp.GetStatus().GetMessage()) + } + + share := resp.GetShare() + if share == nil { + return nil, ErrShareNotFound + } + + if exp := utils.TSToTime(share.GetExpiration()); !exp.IsZero() && exp.Before(time.Now()) { + return nil, ErrShareExpired + } + + return share, nil } diff --git a/services/guestauth/pkg/service/guestauth/service_test.go b/services/guestauth/pkg/service/guestauth/service_test.go index f110fd2f5a..435d5c3550 100644 --- a/services/guestauth/pkg/service/guestauth/service_test.go +++ b/services/guestauth/pkg/service/guestauth/service_test.go @@ -1,17 +1,50 @@ package guestauth import ( + "context" "testing" "time" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/jwt" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + "github.com/opencloud-eu/reva/v2/pkg/utils" + cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) const testShareID = "e0123456-7890-abcd-ef01-234567890abc" +type gatewayTestSelector struct { + client gateway.GatewayAPIClient +} + +func (s gatewayTestSelector) Next(...pool.Option) (gateway.GatewayAPIClient, error) { + return s.client, nil +} + +func newGatewayTestSelector(client gateway.GatewayAPIClient) pool.Selectable[gateway.GatewayAPIClient] { + return gatewayTestSelector{client: client} +} + +func newGatewayMock(resp *collaboration.GetShareResponse) *cs3mocks.GatewayAPIClient { + gwc := &cs3mocks.GatewayAPIClient{} + gwc.On("Authenticate", mock.Anything, mock.Anything). + Return(&gateway.AuthenticateResponse{ + Status: &rpc.Status{Code: rpc.Code_CODE_OK}, + Token: "token", + }, nil) + gwc.On("GetShare", mock.Anything, mock.Anything).Return(resp, nil) + return gwc +} + func newToken(t *testing.T) (string, storage.Record) { ts := token.NewTokenService() tok, err := ts.Generate(testShareID) @@ -31,15 +64,25 @@ func newStorage(t *testing.T) storage.Storage { return storage.NewFileStorage(t.TempDir()) } -func TestVerifyTokenValid(t *testing.T) { - store := newStorage(t) - s := NewGuestAuthService(token.NewTokenService(), store) - tok, rec := newToken(t) - require.NoError(t, store.Add(rec)) +func newShareService(t *testing.T, gwc *cs3mocks.GatewayAPIClient) *GuestAuthService { + t.Helper() + return NewGuestAuthService( + token.NewTokenService(), + newStorage(t), + GatewaySelector(newGatewayTestSelector(gwc)), + ServiceAccount(config.ServiceAccount{ServiceAccountID: "sa-id", ServiceAccountSecret: "sa-secret"}), + ) +} - got, err := s.VerifyToken(tok) - require.NoError(t, err) - assert.Equal(t, rec, got) +func newRedeemService(t *testing.T, store storage.Storage, gwc *cs3mocks.GatewayAPIClient) *GuestAuthService { + t.Helper() + return NewGuestAuthService( + token.NewTokenService(), + store, + GatewaySelector(newGatewayTestSelector(gwc)), + ServiceAccount(config.ServiceAccount{ServiceAccountID: "sa-id", ServiceAccountSecret: "sa-secret"}), + JWT(jwt.NewJwtService("test-secret", time.Hour)), + ) } func TestCreateTokenPersistsRecord(t *testing.T) { @@ -58,24 +101,116 @@ func TestCreateTokenPersistsRecord(t *testing.T) { assert.False(t, rec.Redeemed) } -func TestVerifyTokenExpired(t *testing.T) { - store := newStorage(t) - s := NewGuestAuthService(token.NewTokenService(), store) - tok, rec := newToken(t) - rec.Expiry = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) - require.NoError(t, store.Add(rec)) +func TestVerifyToken(t *testing.T) { + tests := []struct { + name string + expired bool + redeemed bool + wantErr error + }{ + {name: "valid"}, + {name: "expired", expired: true, wantErr: ErrExpired}, + {name: "already redeemed", redeemed: true, wantErr: ErrAlreadyRedeemed}, + } - _, err := s.VerifyToken(tok) - assert.ErrorIs(t, err, ErrExpired) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + store := newStorage(t) + s := NewGuestAuthService(token.NewTokenService(), store) + tok, rec := newToken(t) + if tt.expired { + rec.Expiry = time.Now().Add(-time.Hour) + } + require.NoError(t, store.Add(rec)) + if tt.redeemed { + require.NoError(t, store.Redeem(rec.ShareIDHash)) + } + + got, err := s.verifyToken(tok) + if tt.wantErr != nil { + assert.ErrorIs(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, rec, got) + }) + } } -func TestVerifyTokenAlreadyRedeemed(t *testing.T) { +func TestValidateShare(t *testing.T) { + share := &collaboration.Share{Id: &collaboration.ShareId{OpaqueId: testShareID}} + notExpiredShare := &collaboration.Share{ + Id: &collaboration.ShareId{OpaqueId: testShareID}, + Expiration: utils.TimeToTS(time.Now().Add(time.Hour)), + } + expiredShare := &collaboration.Share{ + Id: &collaboration.ShareId{OpaqueId: testShareID}, + Expiration: utils.TimeToTS(time.Now().Add(-time.Hour)), + } + + tests := []struct { + name string + response *collaboration.GetShareResponse + wantErr error + }{ + { + name: "valid", + response: &collaboration.GetShareResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, Share: share}, + }, + { + name: "not expired", + response: &collaboration.GetShareResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, Share: notExpiredShare}, + }, + { + name: "expired", + response: &collaboration.GetShareResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}, Share: expiredShare}, + wantErr: ErrShareExpired, + }, + { + name: "not found", + response: &collaboration.GetShareResponse{Status: &rpc.Status{Code: rpc.Code_CODE_NOT_FOUND}}, + wantErr: ErrShareNotFound, + }, + { + name: "nil share", + response: &collaboration.GetShareResponse{Status: &rpc.Status{Code: rpc.Code_CODE_OK}}, + wantErr: ErrShareNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := newShareService(t, newGatewayMock(tt.response)) + + got, err := s.validateShare(context.Background(), testShareID) + if tt.wantErr != nil { + assert.ErrorIs(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.response.GetShare(), got) + }) + } +} + +func TestRedeem(t *testing.T) { store := newStorage(t) - s := NewGuestAuthService(token.NewTokenService(), store) tok, rec := newToken(t) require.NoError(t, store.Add(rec)) - require.NoError(t, store.Redeem(rec.ShareIDHash)) - _, err := s.VerifyToken(tok) - assert.ErrorIs(t, err, ErrAlreadyRedeemed) + share := &collaboration.Share{Id: &collaboration.ShareId{OpaqueId: testShareID}} + s := newRedeemService(t, store, newGatewayMock(&collaboration.GetShareResponse{ + Status: &rpc.Status{Code: rpc.Code_CODE_OK}, + Share: share, + })) + + sessionToken, err := s.Redeem(context.Background(), tok) + require.NoError(t, err) + require.NotEmpty(t, sessionToken) + + got, err := store.Get(rec.ShareIDHash) + require.NoError(t, err) + assert.True(t, got.Redeemed) } diff --git a/services/guestauth/pkg/service/jwt/jwt.go b/services/guestauth/pkg/service/jwt/jwt.go new file mode 100644 index 0000000000..3721f7d697 --- /dev/null +++ b/services/guestauth/pkg/service/jwt/jwt.go @@ -0,0 +1,34 @@ +package jwt + +import ( + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type jwtClaims struct { + ShareID string `json:"share_id"` + jwt.RegisteredClaims +} + +type JwtService struct { + secret []byte + ttl time.Duration +} + +func NewJwtService(secret string, ttl time.Duration) *JwtService { + return &JwtService{secret: []byte(secret), ttl: ttl} +} + +// Sign returns a signed jwt token for the given share. +func (m *JwtService) Sign(shareID string) (string, error) { + now := time.Now() + claims := jwtClaims{ + ShareID: shareID, + RegisteredClaims: jwt.RegisteredClaims{ + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(m.ttl)), + }, + } + return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(m.secret) +} diff --git a/services/guestauth/pkg/service/jwt/jwt_test.go b/services/guestauth/pkg/service/jwt/jwt_test.go new file mode 100644 index 0000000000..f2952b7959 --- /dev/null +++ b/services/guestauth/pkg/service/jwt/jwt_test.go @@ -0,0 +1,41 @@ +package jwt + +import ( + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func parseClaims(t *testing.T, m *JwtService, tokenString string) (*jwtClaims, error) { + t.Helper() + c := &jwtClaims{} + _, err := jwt.ParseWithClaims(tokenString, c, func(*jwt.Token) (any, error) { + return m.secret, nil + }, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()})) + return c, err +} + +func TestSignAndParse(t *testing.T) { + m := NewJwtService("test-secret", time.Hour) + + tok, err := m.Sign("share-id") + require.NoError(t, err) + require.NotEmpty(t, tok) + + claims, err := parseClaims(t, m, tok) + require.NoError(t, err) + assert.Equal(t, "share-id", claims.ShareID) +} + +func TestParseExpired(t *testing.T) { + m := NewJwtService("test-secret", -time.Minute) + + tok, err := m.Sign("share-id") + require.NoError(t, err) + + _, err = parseClaims(t, m, tok) + assert.Error(t, err) +} From ffa70f557966a0811eb9b237402fc36b6a0bcc02 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Fri, 25 Sep 2026 12:32:15 +0200 Subject: [PATCH 08/32] feat(guestauth): upd events handlers tests --- services/guestauth/pkg/command/server.go | 2 +- .../pkg/service/events/handlers_test.go | 60 +++++++++++++++++-- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/services/guestauth/pkg/command/server.go b/services/guestauth/pkg/command/server.go index 1df914c706..feab1ff39d 100644 --- a/services/guestauth/pkg/command/server.go +++ b/services/guestauth/pkg/command/server.go @@ -75,7 +75,7 @@ func Server(cfg *config.Config) *cobra.Command { tokenSvc := token.NewTokenService() store := storage.NewFileStorage(cfg.Storage.RootDirectory) jwtService := jwt.NewJwtService(cfg.TokenManager.JWTSecret, cfg.JWT.TTL) - + guestAuth := guestauth.NewGuestAuthService(tokenSvc, store, guestauth.GatewaySelector(gatewaySelector), guestauth.ServiceAccount(cfg.ServiceAccount), diff --git a/services/guestauth/pkg/service/events/handlers_test.go b/services/guestauth/pkg/service/events/handlers_test.go index f5ad313420..69575bcf3d 100644 --- a/services/guestauth/pkg/service/events/handlers_test.go +++ b/services/guestauth/pkg/service/events/handlers_test.go @@ -5,17 +5,34 @@ import ( "testing" "time" + user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + ocEvents "github.com/opencloud-eu/opencloud/pkg/events" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" "github.com/opencloud-eu/reva/v2/pkg/events" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + microevents "go-micro.dev/v4/events" ) const testShareID = "e0123456-7890-abcd-ef01-234567890abc" +type testBus struct { + published []any +} + +func (tb *testBus) Publish(_ string, ev any, _ ...microevents.PublishOption) error { + tb.published = append(tb.published, ev) + return nil +} + +func (tb *testBus) Consume(_ string, _ ...microevents.ConsumeOption) (<-chan microevents.Event, error) { + return nil, nil +} + func addRecord(t *testing.T, store storage.Storage, shareID string) storage.Record { ts := token.NewTokenService() tok, err := ts.Generate(shareID) @@ -32,17 +49,50 @@ func addRecord(t *testing.T, store storage.Storage, shareID string) storage.Reco return rec } -func newConsumer(t *testing.T) (*EventConsumer, storage.Storage) { +func newConsumer(t *testing.T) (*EventConsumer, storage.Storage, *testBus) { + t.Helper() store := storage.NewFileStorage(t.TempDir()) guestAuth := guestauth.NewGuestAuthService(token.NewTokenService(), store) - consumer, err := NewEventConsumer(nil, GuestAuthService(guestAuth)) + bus := &testBus{} + consumer, err := NewEventConsumer(bus, GuestAuthService(guestAuth)) require.NoError(t, err) - return consumer, store + return consumer, store, bus +} + +func TestHandleShareCreated(t *testing.T) { + svc, store, bus := newConsumer(t) + + ev := events.ShareCreated{ + ShareID: &collaboration.ShareId{OpaqueId: testShareID}, + Sharer: &user.UserId{OpaqueId: "sharer"}, + ItemID: &provider.ResourceId{StorageId: "storage", OpaqueId: "item"}, + ResourceName: "resource", + GranteeUserID: &user.UserId{OpaqueId: "guest", Type: user.UserType_USER_TYPE_GUEST}, + } + + require.NoError(t, svc.handleShareCreated(context.Background(), ev)) + + rec, err := store.Get(token.Hash(testShareID)) + require.NoError(t, err) + assert.Equal(t, testShareID, rec.ShareID) + + require.Len(t, bus.published, 1) + published, ok := bus.published[0].(ocEvents.GuestTokenCreated) + require.True(t, ok) + assert.Equal(t, testShareID, published.ShareID.GetOpaqueId()) + assert.Equal(t, ev.Sharer, published.Sharer) + assert.Equal(t, ev.ItemID, published.ItemID) + assert.Equal(t, ev.ResourceName, published.ResourceName) + + parsed, err := token.NewTokenService().Parse(published.Token) + require.NoError(t, err) + assert.Equal(t, token.Hash(testShareID), parsed.ShareIDHash) + assert.Equal(t, rec.SecretHash, parsed.SecretHash) } func TestHandleShareRemoved(t *testing.T) { - svc, store := newConsumer(t) + svc, store, _ := newConsumer(t) rec := addRecord(t, store, testShareID) ev := events.ShareRemoved{ @@ -56,7 +106,7 @@ func TestHandleShareRemoved(t *testing.T) { } func TestHandleShareExpired(t *testing.T) { - svc, store := newConsumer(t) + svc, store, _ := newConsumer(t) rec := addRecord(t, store, testShareID) ev := events.ShareExpired{ From 2fc5fb36c3397b748411d116ac2bcae333c801bb Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Fri, 25 Sep 2026 13:00:21 +0200 Subject: [PATCH 09/32] feat(guestauth): used renamio for file write --- go.mod | 2 +- .../pkg/service/storage/file_storage.go | 32 +++---------------- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index 44cfe9ee4b..edf987e782 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,7 @@ require ( github.com/golang/protobuf v1.5.4 github.com/google/go-cmp v0.7.0 github.com/google/go-tika v0.3.1 + github.com/google/renameio/v2 v2.0.2 github.com/google/uuid v1.6.0 github.com/gookit/config/v2 v2.2.9 github.com/gorilla/mux v1.8.1 @@ -235,7 +236,6 @@ require ( github.com/google/go-querystring v1.2.0 // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect - github.com/google/renameio/v2 v2.0.2 // indirect github.com/gookit/goutil v0.8.0 // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/schema v1.4.1 // indirect diff --git a/services/guestauth/pkg/service/storage/file_storage.go b/services/guestauth/pkg/service/storage/file_storage.go index 97fa06e793..8f7aa2557f 100644 --- a/services/guestauth/pkg/service/storage/file_storage.go +++ b/services/guestauth/pkg/service/storage/file_storage.go @@ -8,6 +8,8 @@ import ( "os" "path/filepath" "sync" + + "github.com/google/renameio/v2" ) func NewFileStorage(root string) *FileStorage { @@ -22,6 +24,7 @@ type FileStorage struct { } const dirPerm = 0700 +const filePerm = 0600 func (s *FileStorage) Add(rec Record) error { s.mu.Lock() @@ -79,34 +82,7 @@ func (s *FileStorage) add(rec Record) error { return fmt.Errorf("could not create directory %s: %w", dir, err) } - // Create temporary file is needed to ensure that if something is wrong during write we will not have - // a corupted file on disk which can be later treated as a valid file which contains a token record. - f, err := os.CreateTemp(dir, "tmpguestauth") - if err != nil { - return fmt.Errorf("could not create temporary file for %s: %w", rec.ShareIDHash, err) - } - defer f.Close() - - if _, writeErr := f.Write(data); writeErr != nil { - if remErr := os.Remove(f.Name()); remErr != nil { - return fmt.Errorf("could not cleanup temporary file for %s: %w", rec.ShareIDHash, remErr) - } - return fmt.Errorf("could not write temporary file for %s: %w", rec.ShareIDHash, writeErr) - } - - // just in case there is a simultan write of the identic file(record) - if synErr := f.Sync(); synErr != nil { - return fmt.Errorf("could not sync temporary file for %s: %w", rec.ShareIDHash, synErr) - } - - if renErr := os.Rename(f.Name(), p); renErr != nil { - if remErr := os.Remove(f.Name()); remErr != nil { - return fmt.Errorf("rename failed and could not cleanup temporary file for %s: %w", rec.ShareIDHash, remErr) - } - return fmt.Errorf("could not rename temporary file to %s: %w", p, renErr) - } - - return nil + return renameio.WriteFile(p, data, filePerm) } func (s *FileStorage) get(shareIDHash string) (Record, error) { From a1cb335c37e160053c7fc9e1b274186ae145bf6d Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Fri, 25 Sep 2026 13:54:54 +0200 Subject: [PATCH 10/32] feat(guestauth): register gusetauth service into opencloud config --- Makefile | 1 + opencloud/pkg/command/services.go | 6 ++++++ opencloud/pkg/init/init.go | 16 ++++++++++++++++ opencloud/pkg/init/structs.go | 7 +++++++ opencloud/pkg/runtime/service/service.go | 6 ++++++ pkg/config/config.go | 2 ++ pkg/config/defaultconfig.go | 2 ++ 7 files changed, 40 insertions(+) diff --git a/Makefile b/Makefile index d014931b02..23f2a9275d 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,7 @@ OC_MODULES = \ services/gateway \ services/graph \ services/groups \ + services/guestauth \ services/idm \ services/idp \ services/invitations \ diff --git a/opencloud/pkg/command/services.go b/opencloud/pkg/command/services.go index 3c0d3852d0..6364be5d7e 100644 --- a/opencloud/pkg/command/services.go +++ b/opencloud/pkg/command/services.go @@ -24,6 +24,7 @@ import ( gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/command" graph "github.com/opencloud-eu/opencloud/services/graph/pkg/command" groups "github.com/opencloud-eu/opencloud/services/groups/pkg/command" + guestauth "github.com/opencloud-eu/opencloud/services/guestauth/pkg/command" idm "github.com/opencloud-eu/opencloud/services/idm/pkg/command" idp "github.com/opencloud-eu/opencloud/services/idp/pkg/command" invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/command" @@ -138,6 +139,11 @@ var serviceCommands = []register.Command{ cfg.Groups.Commons = cfg.Commons }) }, + func(cfg *config.Config) *cobra.Command { + return ServiceCommand(cfg, cfg.GuestAuth.Service.Name, guestauth.GetCommands(cfg.GuestAuth), func(c *config.Config) { + cfg.GuestAuth.Commons = cfg.Commons + }) + }, func(cfg *config.Config) *cobra.Command { return ServiceCommand(cfg, cfg.IDM.Service.Name, idm.GetCommands(cfg.IDM), func(c *config.Config) { cfg.IDM.Commons = cfg.Commons diff --git a/opencloud/pkg/init/init.go b/opencloud/pkg/init/init.go index 83ae39e453..511fafd293 100644 --- a/opencloud/pkg/init/init.go +++ b/opencloud/pkg/init/init.go @@ -69,6 +69,7 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword idmServicePassword, idpServicePassword, ocAdminServicePassword, revaServicePassword string tokenManagerJwtSecret, collaborationWOPISecret, machineAuthAPIKey, systemUserAPIKey string revaTransferSecret, thumbnailsTransferSecret, serviceAccountSecret, urlSigningSecret string + guestAuthJWTSecret string adminPasswdwordGenerated bool ) @@ -103,6 +104,13 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword return fmt.Errorf("could not generate random secret for urlSigningSecret: %s", err) } } + guestAuthJWTSecret = oldCfg.GuestAuth.TokenManager.JWTSecret + if guestAuthJWTSecret == "" { + guestAuthJWTSecret, err = generators.GenerateRandomPassword(passwordLength) + if err != nil { + return fmt.Errorf("could not generate random secret for guestAuthJWTSecret: %s", err) + } + } } else { systemUserID = uuid.NewString() adminUserID = uuid.NewString() @@ -155,6 +163,10 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword if err != nil { return fmt.Errorf("could not generate random secret for urlSigningSecret: %s", err) } + guestAuthJWTSecret, err = generators.GenerateRandomPassword(passwordLength) + if err != nil { + return fmt.Errorf("could not generate random secret for guestAuthJWTSecret: %s", err) + } thumbnailsTransferSecret, err = generators.GenerateRandomPassword(passwordLength) if err != nil { return fmt.Errorf("could not generate random password for thumbnailsTransferSecret: %s", err) @@ -212,6 +224,10 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword }, }, }, + GuestAuth: GuestAuth{ + ServiceAccount: serviceAccount, + TokenManager: TokenManager{JWTSecret: guestAuthJWTSecret}, + }, Users: UsersAndGroupsService{ Drivers: LdapBasedService{ Ldap: LdapSettings{ diff --git a/opencloud/pkg/init/structs.go b/opencloud/pkg/init/structs.go index 3e6d0ae4f9..c3362c1c0f 100644 --- a/opencloud/pkg/init/structs.go +++ b/opencloud/pkg/init/structs.go @@ -32,6 +32,7 @@ type OpenCloudConfig struct { AuthBearer AuthbearerService `yaml:"auth_bearer"` Users UsersAndGroupsService `yaml:"users"` Groups UsersAndGroupsService `yaml:"groups"` + GuestAuth GuestAuth `yaml:"guestauth"` Ocm OcmService `yaml:"ocm"` Thumbnails ThumbnailService `yaml:"thumbnails"` Search Search `yaml:"search"` @@ -53,6 +54,12 @@ type Activitylog struct { ServiceAccount ServiceAccount `yaml:"service_account"` } +// GuestAuth is the configuration for the guestauth service +type GuestAuth struct { + ServiceAccount ServiceAccount `yaml:"service_account"` + TokenManager TokenManager `yaml:"token_manager"` +} + // App is the configuration for the collaboration service type App struct { Insecure bool `yaml:"insecure"` diff --git a/opencloud/pkg/runtime/service/service.go b/opencloud/pkg/runtime/service/service.go index c360a623f2..c002621209 100644 --- a/opencloud/pkg/runtime/service/service.go +++ b/opencloud/pkg/runtime/service/service.go @@ -35,6 +35,7 @@ import ( gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/command" graph "github.com/opencloud-eu/opencloud/services/graph/pkg/command" groups "github.com/opencloud-eu/opencloud/services/groups/pkg/command" + guestauth "github.com/opencloud-eu/opencloud/services/guestauth/pkg/command" idm "github.com/opencloud-eu/opencloud/services/idm/pkg/command" idp "github.com/opencloud-eu/opencloud/services/idp/pkg/command" invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/command" @@ -198,6 +199,11 @@ func NewService(ctx context.Context, options ...Option) (*Service, error) { cfg.Groups.Commons = cfg.Commons return groups.Execute(cfg.Groups) }) + reg(3, opts.Config.GuestAuth.Service.Name, func(ctx context.Context, cfg *occfg.Config) error { + cfg.GuestAuth.Context = ctx + cfg.GuestAuth.Commons = cfg.Commons + return guestauth.Execute(cfg.GuestAuth) + }) reg(3, opts.Config.IDM.Service.Name, func(ctx context.Context, cfg *occfg.Config) error { cfg.IDM.Context = ctx cfg.IDM.Commons = cfg.Commons diff --git a/pkg/config/config.go b/pkg/config/config.go index 36536c8064..9cf8f91ed5 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -19,6 +19,7 @@ import ( gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/config" graph "github.com/opencloud-eu/opencloud/services/graph/pkg/config" groups "github.com/opencloud-eu/opencloud/services/groups/pkg/config" + guestauth "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" idm "github.com/opencloud-eu/opencloud/services/idm/pkg/config" idp "github.com/opencloud-eu/opencloud/services/idp/pkg/config" invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/config" @@ -99,6 +100,7 @@ type Config struct { Gateway *gateway.Config `yaml:"gateway"` Graph *graph.Config `yaml:"graph"` Groups *groups.Config `yaml:"groups"` + GuestAuth *guestauth.Config `yaml:"guestauth"` IDM *idm.Config `yaml:"idm"` IDP *idp.Config `yaml:"idp"` Invitations *invitations.Config `yaml:"invitations"` diff --git a/pkg/config/defaultconfig.go b/pkg/config/defaultconfig.go index 59f5de02b2..392184b2ee 100644 --- a/pkg/config/defaultconfig.go +++ b/pkg/config/defaultconfig.go @@ -19,6 +19,7 @@ import ( gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/config/defaults" graph "github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults" groups "github.com/opencloud-eu/opencloud/services/groups/pkg/config/defaults" + guestauth "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config/defaults" idm "github.com/opencloud-eu/opencloud/services/idm/pkg/config/defaults" idp "github.com/opencloud-eu/opencloud/services/idp/pkg/config/defaults" invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/config/defaults" @@ -74,6 +75,7 @@ func DefaultConfig() *Config { Gateway: gateway.DefaultConfig(), Graph: graph.DefaultConfig(), Groups: groups.DefaultConfig(), + GuestAuth: guestauth.DefaultConfig(), IDM: idm.DefaultConfig(), IDP: idp.DefaultConfig(), Invitations: invitations.DefaultConfig(), From 402a0c04a170625ddc324fd010686e4fec010c66 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Fri, 25 Sep 2026 14:14:38 +0200 Subject: [PATCH 11/32] feat(guestauth): register guestauth proxy --- services/proxy/pkg/config/defaults/defaultconfig.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index 8b43147a2b..0db7029fb1 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -283,6 +283,11 @@ func DefaultPolicies() []config.Policy { Endpoint: "/graph/v1.0/invitations", Service: "eu.opencloud.web.invitations", }, + { + Endpoint: "/graph/v1beta1/guestInvitations", + Service: "eu.opencloud.web.guestauth", + Unprotected: true, + }, { Endpoint: "/graph/", Service: "eu.opencloud.web.graph", From 6ea563de9635fb85b9604c08bce34817db5bc85b Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Fri, 25 Sep 2026 16:14:36 +0200 Subject: [PATCH 12/32] feat(guestauth): added mocks for tests, made services dependencies to be based on interfaces --- services/guestauth/.mockery.yaml | 14 + services/guestauth/Makefile | 6 +- services/guestauth/pkg/server/http/option.go | 4 +- services/guestauth/pkg/server/http/redeem.go | 2 +- .../guestauth/pkg/server/http/redeem_test.go | 99 +++---- .../guestauth/pkg/service/events/handlers.go | 2 +- .../pkg/service/events/handlers_test.go | 73 +++-- .../guestauth/pkg/service/events/options.go | 4 +- .../guestauth/pkg/service/events/service.go | 2 +- .../pkg/service/guestauth/mocks/guest_auth.go | 224 ++++++++++++++++ .../pkg/service/guestauth/service.go | 46 +++- .../pkg/service/guestauth/service_test.go | 76 ++++-- .../pkg/service/storage/file_storage.go | 4 + .../pkg/service/storage/file_storage_test.go | 3 + .../pkg/service/storage/mocks/storage.go | 250 ++++++++++++++++++ .../guestauth/pkg/service/storage/storage.go | 4 +- 16 files changed, 671 insertions(+), 142 deletions(-) create mode 100644 services/guestauth/.mockery.yaml create mode 100644 services/guestauth/pkg/service/guestauth/mocks/guest_auth.go create mode 100644 services/guestauth/pkg/service/storage/mocks/storage.go diff --git a/services/guestauth/.mockery.yaml b/services/guestauth/.mockery.yaml new file mode 100644 index 0000000000..8b68d395af --- /dev/null +++ b/services/guestauth/.mockery.yaml @@ -0,0 +1,14 @@ +# maintain v2 separate mocks dir +dir: "{{.InterfaceDir}}/mocks" +structname: "{{.InterfaceName}}" +filename: "{{.InterfaceName | snakecase }}.go" +pkgname: mocks + +template: testify +packages: + github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth: + interfaces: + GuestAuth: {} + github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage: + interfaces: + Storage: {} diff --git a/services/guestauth/Makefile b/services/guestauth/Makefile index 4cae818a68..9a191622a7 100644 --- a/services/guestauth/Makefile +++ b/services/guestauth/Makefile @@ -8,4 +8,8 @@ endif include ../../.make/default.mk include ../../.make/go.mk include ../../.make/release.mk -include ../../.make/docs.mk \ No newline at end of file +include ../../.make/docs.mk + +.PHONY: go-generate +go-generate: $(MOCKERY) + $(MOCKERY) diff --git a/services/guestauth/pkg/server/http/option.go b/services/guestauth/pkg/server/http/option.go index bc897b2a14..5c5c54ac51 100644 --- a/services/guestauth/pkg/server/http/option.go +++ b/services/guestauth/pkg/server/http/option.go @@ -17,7 +17,7 @@ type Options struct { Logger log.Logger Context context.Context Config *config.Config - Service *guestauth.GuestAuthService + Service guestauth.GuestAuth Flags []pflag.Flag } @@ -54,7 +54,7 @@ func Config(val *config.Config) Option { } // Service provides a function to set the service option. -func Service(val *guestauth.GuestAuthService) Option { +func Service(val guestauth.GuestAuth) Option { return func(o *Options) { o.Service = val } diff --git a/services/guestauth/pkg/server/http/redeem.go b/services/guestauth/pkg/server/http/redeem.go index e3c386a806..192f97ae47 100644 --- a/services/guestauth/pkg/server/http/redeem.go +++ b/services/guestauth/pkg/server/http/redeem.go @@ -18,7 +18,7 @@ type RedeemRequest struct { } // RedeemHandler validates the token submitted to the redeem endpoint. -func RedeemHandler(log log.Logger, s *guestauth.GuestAuthService, cfg *config.Config) func(w http.ResponseWriter, r *http.Request) { +func RedeemHandler(log log.Logger, s guestauth.GuestAuth, cfg *config.Config) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var req RedeemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { diff --git a/services/guestauth/pkg/server/http/redeem_test.go b/services/guestauth/pkg/server/http/redeem_test.go index 17b4c13597..02987abcd7 100644 --- a/services/guestauth/pkg/server/http/redeem_test.go +++ b/services/guestauth/pkg/server/http/redeem_test.go @@ -8,57 +8,19 @@ import ( "testing" "time" - gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" - rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" - collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/jwt" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth/mocks" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" - "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" - cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) -const testShareID = "e0123456-7890-abcd-ef01-234567890abc" - -type gatewayTestSelector struct { - client gateway.GatewayAPIClient -} - -func (s gatewayTestSelector) Next(...pool.Option) (gateway.GatewayAPIClient, error) { - return s.client, nil -} - -func newGatewayMock() *cs3mocks.GatewayAPIClient { - gwc := &cs3mocks.GatewayAPIClient{} - gwc.On("Authenticate", mock.Anything, mock.Anything). - Return(&gateway.AuthenticateResponse{ - Status: &rpc.Status{Code: rpc.Code_CODE_OK}, - Token: "token", - }, nil) - gwc.On("GetShare", mock.Anything, mock.Anything).Return(&collaboration.GetShareResponse{ - Status: &rpc.Status{Code: rpc.Code_CODE_OK}, - Share: &collaboration.Share{ - Id: &collaboration.ShareId{OpaqueId: testShareID}, - }, - }, nil) - return gwc -} - -func newRedeemHandler(t *testing.T, store storage.Storage) http.HandlerFunc { +func newRedeemHandler(t *testing.T, svc guestauth.GuestAuth) http.HandlerFunc { t.Helper() - svc := guestauth.NewGuestAuthService( - token.NewTokenService(), - store, - guestauth.GatewaySelector(gatewayTestSelector{client: newGatewayMock()}), - guestauth.ServiceAccount(config.ServiceAccount{ServiceAccountID: "sa-id", ServiceAccountSecret: "sa-secret"}), - guestauth.JWT(jwt.NewJwtService("test-secret", time.Hour)), - ) cfg := &config.Config{ JWT: config.JWT{ CookieName: "oc_guest_session", @@ -70,21 +32,14 @@ func newRedeemHandler(t *testing.T, store storage.Storage) http.HandlerFunc { } func TestRedeemHandler(t *testing.T) { - store := storage.NewFileStorage(t.TempDir()) - ts := token.NewTokenService() - tok, err := ts.Generate(testShareID) - require.NoError(t, err) - require.NoError(t, store.Add(storage.Record{ - ShareID: testShareID, - ShareIDHash: tok.ShareIDHash, - SecretHash: tok.SecretHash, - })) + svcMock := mocks.NewGuestAuth(t) + svcMock.On("Redeem", mock.Anything, "valid-token").Return("session-token", nil) - body, err := json.Marshal(RedeemRequest{Token: tok.String()}) + body, err := json.Marshal(RedeemRequest{Token: "valid-token"}) require.NoError(t, err) rr := httptest.NewRecorder() - newRedeemHandler(t, store)(rr, httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(body)))) + newRedeemHandler(t, svcMock)(rr, httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(body)))) assert.Equal(t, http.StatusOK, rr.Code) @@ -95,18 +50,46 @@ func TestRedeemHandler(t *testing.T) { } } require.NotNil(t, cookie) + assert.Equal(t, "session-token", cookie.Value) assert.True(t, cookie.HttpOnly) assert.Equal(t, "/", cookie.Path) - assert.NotEmpty(t, cookie.Value) } -func TestRedeemHandlerInvalidToken(t *testing.T) { - store := storage.NewFileStorage(t.TempDir()) - body, err := json.Marshal(RedeemRequest{Token: "not-a-valid-token"}) - require.NoError(t, err) +func TestRedeemHandlerErrorMapping(t *testing.T) { + tests := []struct { + name string + err error + wantStatus int + }{ + {name: "already redeemed", err: guestauth.ErrAlreadyRedeemed, wantStatus: http.StatusConflict}, + {name: "token expired", err: guestauth.ErrExpired, wantStatus: http.StatusGone}, + {name: "token not found", err: storage.ErrNotFound, wantStatus: http.StatusNotFound}, + {name: "invalid token", err: token.ErrInvalidToken, wantStatus: http.StatusUnauthorized}, + {name: "share not found", err: guestauth.ErrShareNotFound, wantStatus: http.StatusNotFound}, + {name: "share expired", err: guestauth.ErrShareExpired, wantStatus: http.StatusGone}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + svcMock := mocks.NewGuestAuth(t) + svcMock.On("Redeem", mock.Anything, "token").Return("", tt.err) + + body, err := json.Marshal(RedeemRequest{Token: "token"}) + require.NoError(t, err) + + rr := httptest.NewRecorder() + newRedeemHandler(t, svcMock)(rr, httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(body)))) + + assert.Equal(t, tt.wantStatus, rr.Code) + }) + } +} + +func TestRedeemHandlerMalformedBody(t *testing.T) { + svcMock := mocks.NewGuestAuth(t) rr := httptest.NewRecorder() - newRedeemHandler(t, store)(rr, httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(body)))) + newRedeemHandler(t, svcMock)(rr, httptest.NewRequest(http.MethodPost, "/", strings.NewReader("not-json"))) - assert.Equal(t, http.StatusUnauthorized, rr.Code) + assert.Equal(t, http.StatusBadRequest, rr.Code) } diff --git a/services/guestauth/pkg/service/events/handlers.go b/services/guestauth/pkg/service/events/handlers.go index f736bea6d2..c86a4e22b2 100644 --- a/services/guestauth/pkg/service/events/handlers.go +++ b/services/guestauth/pkg/service/events/handlers.go @@ -19,7 +19,7 @@ func (s *EventConsumer) handleShareCreated(ctx context.Context, ev events.ShareC return nil } - tok, err := s.guestAuth.CreateToken(ev.ShareID.GetOpaqueId()) + tok, err := s.guestAuth.CreateToken(ctx, ev.ShareID.GetOpaqueId()) if err != nil { return err } diff --git a/services/guestauth/pkg/service/events/handlers_test.go b/services/guestauth/pkg/service/events/handlers_test.go index 69575bcf3d..c19f516816 100644 --- a/services/guestauth/pkg/service/events/handlers_test.go +++ b/services/guestauth/pkg/service/events/handlers_test.go @@ -3,17 +3,17 @@ package events import ( "context" "testing" - "time" user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" ocEvents "github.com/opencloud-eu/opencloud/pkg/events" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth/mocks" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" "github.com/opencloud-eu/reva/v2/pkg/events" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" microevents "go-micro.dev/v4/events" ) @@ -33,35 +33,23 @@ func (tb *testBus) Consume(_ string, _ ...microevents.ConsumeOption) (<-chan mic return nil, nil } -func addRecord(t *testing.T, store storage.Storage, shareID string) storage.Record { - ts := token.NewTokenService() - tok, err := ts.Generate(shareID) - require.NoError(t, err) - - rec := storage.Record{ - ShareID: shareID, - ShareIDHash: tok.ShareIDHash, - SecretHash: tok.SecretHash, - Expiry: time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC), - } - require.NoError(t, store.Add(rec)) - - return rec -} - -func newConsumer(t *testing.T) (*EventConsumer, storage.Storage, *testBus) { +func newConsumer(t *testing.T, guestAuth guestauth.GuestAuth) (*EventConsumer, *testBus) { t.Helper() - store := storage.NewFileStorage(t.TempDir()) - guestAuth := guestauth.NewGuestAuthService(token.NewTokenService(), store) bus := &testBus{} consumer, err := NewEventConsumer(bus, GuestAuthService(guestAuth)) require.NoError(t, err) - return consumer, store, bus + return consumer, bus } func TestHandleShareCreated(t *testing.T) { - svc, store, bus := newConsumer(t) + tok, err := token.NewTokenService().Generate(testShareID) + require.NoError(t, err) + + svcMock := mocks.NewGuestAuth(t) + svcMock.On("CreateToken", mock.Anything, testShareID).Return(tok, nil) + + svc, bus := newConsumer(t, svcMock) ev := events.ShareCreated{ ShareID: &collaboration.ShareId{OpaqueId: testShareID}, @@ -73,9 +61,7 @@ func TestHandleShareCreated(t *testing.T) { require.NoError(t, svc.handleShareCreated(context.Background(), ev)) - rec, err := store.Get(token.Hash(testShareID)) - require.NoError(t, err) - assert.Equal(t, testShareID, rec.ShareID) + svcMock.AssertCalled(t, "CreateToken", mock.Anything, testShareID) require.Len(t, bus.published, 1) published, ok := bus.published[0].(ocEvents.GuestTokenCreated) @@ -84,16 +70,28 @@ func TestHandleShareCreated(t *testing.T) { assert.Equal(t, ev.Sharer, published.Sharer) assert.Equal(t, ev.ItemID, published.ItemID) assert.Equal(t, ev.ResourceName, published.ResourceName) + assert.Equal(t, tok.String(), published.Token) +} - parsed, err := token.NewTokenService().Parse(published.Token) - require.NoError(t, err) - assert.Equal(t, token.Hash(testShareID), parsed.ShareIDHash) - assert.Equal(t, rec.SecretHash, parsed.SecretHash) +func TestHandleShareCreatedSkipsNonGuest(t *testing.T) { + svcMock := mocks.NewGuestAuth(t) + svc, bus := newConsumer(t, svcMock) + + ev := events.ShareCreated{ + ShareID: &collaboration.ShareId{OpaqueId: testShareID}, + GranteeUserID: &user.UserId{OpaqueId: "user", Type: user.UserType_USER_TYPE_PRIMARY}, + } + + require.NoError(t, svc.handleShareCreated(context.Background(), ev)) + + svcMock.AssertNotCalled(t, "CreateToken", mock.Anything, mock.Anything) + assert.Empty(t, bus.published) } func TestHandleShareRemoved(t *testing.T) { - svc, store, _ := newConsumer(t) - rec := addRecord(t, store, testShareID) + svcMock := mocks.NewGuestAuth(t) + svcMock.On("CleanupShare", testShareID).Return(nil) + svc, _ := newConsumer(t, svcMock) ev := events.ShareRemoved{ ShareID: &collaboration.ShareId{OpaqueId: testShareID}, @@ -101,13 +99,13 @@ func TestHandleShareRemoved(t *testing.T) { require.NoError(t, svc.handleShareRemoved(context.Background(), ev)) - _, err := store.Get(rec.ShareIDHash) - assert.ErrorIs(t, err, storage.ErrNotFound) + svcMock.AssertCalled(t, "CleanupShare", testShareID) } func TestHandleShareExpired(t *testing.T) { - svc, store, _ := newConsumer(t) - rec := addRecord(t, store, testShareID) + svcMock := mocks.NewGuestAuth(t) + svcMock.On("CleanupShare", testShareID).Return(nil) + svc, _ := newConsumer(t, svcMock) ev := events.ShareExpired{ ShareID: &collaboration.ShareId{OpaqueId: testShareID}, @@ -115,6 +113,5 @@ func TestHandleShareExpired(t *testing.T) { require.NoError(t, svc.handleShareExpired(context.Background(), ev)) - _, err := store.Get(rec.ShareIDHash) - assert.ErrorIs(t, err, storage.ErrNotFound) + svcMock.AssertCalled(t, "CleanupShare", testShareID) } diff --git a/services/guestauth/pkg/service/events/options.go b/services/guestauth/pkg/service/events/options.go index b82e03da0f..a3f8e10d96 100644 --- a/services/guestauth/pkg/service/events/options.go +++ b/services/guestauth/pkg/service/events/options.go @@ -18,7 +18,7 @@ type Options struct { Stream events.Stream RegisteredEvents []events.Unmarshaller NumConsumers int - GuestAuthService *guestauth.GuestAuthService + GuestAuthService guestauth.GuestAuth } // Context configures a context for the guestauth service @@ -57,7 +57,7 @@ func NumConsumers(num int) Option { } // GuestAuthService configures the guest auth domain service. -func GuestAuthService(s *guestauth.GuestAuthService) Option { +func GuestAuthService(s guestauth.GuestAuth) Option { return func(o *Options) { o.GuestAuthService = s } diff --git a/services/guestauth/pkg/service/events/service.go b/services/guestauth/pkg/service/events/service.go index beab55323b..01304769e8 100644 --- a/services/guestauth/pkg/service/events/service.go +++ b/services/guestauth/pkg/service/events/service.go @@ -28,7 +28,7 @@ type EventConsumer struct { log log.Logger stream events.Stream - guestAuth *guestauth.GuestAuthService + guestAuth guestauth.GuestAuth numConsumers int diff --git a/services/guestauth/pkg/service/guestauth/mocks/guest_auth.go b/services/guestauth/pkg/service/guestauth/mocks/guest_auth.go new file mode 100644 index 0000000000..eb39880608 --- /dev/null +++ b/services/guestauth/pkg/service/guestauth/mocks/guest_auth.go @@ -0,0 +1,224 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "context" + + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" + mock "github.com/stretchr/testify/mock" +) + +// NewGuestAuth creates a new instance of GuestAuth. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewGuestAuth(t interface { + mock.TestingT + Cleanup(func()) +}) *GuestAuth { + mock := &GuestAuth{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// GuestAuth is an autogenerated mock type for the GuestAuth type +type GuestAuth struct { + mock.Mock +} + +type GuestAuth_Expecter struct { + mock *mock.Mock +} + +func (_m *GuestAuth) EXPECT() *GuestAuth_Expecter { + return &GuestAuth_Expecter{mock: &_m.Mock} +} + +// CleanupShare provides a mock function for the type GuestAuth +func (_mock *GuestAuth) CleanupShare(shareID string) error { + ret := _mock.Called(shareID) + + if len(ret) == 0 { + panic("no return value specified for CleanupShare") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string) error); ok { + r0 = returnFunc(shareID) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// GuestAuth_CleanupShare_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CleanupShare' +type GuestAuth_CleanupShare_Call struct { + *mock.Call +} + +// CleanupShare is a helper method to define mock.On call +// - shareID string +func (_e *GuestAuth_Expecter) CleanupShare(shareID any) *GuestAuth_CleanupShare_Call { + return &GuestAuth_CleanupShare_Call{Call: _e.mock.On("CleanupShare", shareID)} +} + +func (_c *GuestAuth_CleanupShare_Call) Run(run func(shareID string)) *GuestAuth_CleanupShare_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *GuestAuth_CleanupShare_Call) Return(err error) *GuestAuth_CleanupShare_Call { + _c.Call.Return(err) + return _c +} + +func (_c *GuestAuth_CleanupShare_Call) RunAndReturn(run func(shareID string) error) *GuestAuth_CleanupShare_Call { + _c.Call.Return(run) + return _c +} + +// CreateToken provides a mock function for the type GuestAuth +func (_mock *GuestAuth) CreateToken(ctx context.Context, shareID string) (*token.Token, error) { + ret := _mock.Called(ctx, shareID) + + if len(ret) == 0 { + panic("no return value specified for CreateToken") + } + + var r0 *token.Token + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*token.Token, error)); ok { + return returnFunc(ctx, shareID) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *token.Token); ok { + r0 = returnFunc(ctx, shareID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*token.Token) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = returnFunc(ctx, shareID) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// GuestAuth_CreateToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateToken' +type GuestAuth_CreateToken_Call struct { + *mock.Call +} + +// CreateToken is a helper method to define mock.On call +// - ctx context.Context +// - shareID string +func (_e *GuestAuth_Expecter) CreateToken(ctx any, shareID any) *GuestAuth_CreateToken_Call { + return &GuestAuth_CreateToken_Call{Call: _e.mock.On("CreateToken", ctx, shareID)} +} + +func (_c *GuestAuth_CreateToken_Call) Run(run func(ctx context.Context, shareID string)) *GuestAuth_CreateToken_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *GuestAuth_CreateToken_Call) Return(token1 *token.Token, err error) *GuestAuth_CreateToken_Call { + _c.Call.Return(token1, err) + return _c +} + +func (_c *GuestAuth_CreateToken_Call) RunAndReturn(run func(ctx context.Context, shareID string) (*token.Token, error)) *GuestAuth_CreateToken_Call { + _c.Call.Return(run) + return _c +} + +// Redeem provides a mock function for the type GuestAuth +func (_mock *GuestAuth) Redeem(ctx context.Context, tokenString string) (string, error) { + ret := _mock.Called(ctx, tokenString) + + if len(ret) == 0 { + panic("no return value specified for Redeem") + } + + var r0 string + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok { + return returnFunc(ctx, tokenString) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) string); ok { + r0 = returnFunc(ctx, tokenString) + } else { + r0 = ret.Get(0).(string) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = returnFunc(ctx, tokenString) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// GuestAuth_Redeem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Redeem' +type GuestAuth_Redeem_Call struct { + *mock.Call +} + +// Redeem is a helper method to define mock.On call +// - ctx context.Context +// - tokenString string +func (_e *GuestAuth_Expecter) Redeem(ctx any, tokenString any) *GuestAuth_Redeem_Call { + return &GuestAuth_Redeem_Call{Call: _e.mock.On("Redeem", ctx, tokenString)} +} + +func (_c *GuestAuth_Redeem_Call) Run(run func(ctx context.Context, tokenString string)) *GuestAuth_Redeem_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *GuestAuth_Redeem_Call) Return(s string, err error) *GuestAuth_Redeem_Call { + _c.Call.Return(s, err) + return _c +} + +func (_c *GuestAuth_Redeem_Call) RunAndReturn(run func(ctx context.Context, tokenString string) (string, error)) *GuestAuth_Redeem_Call { + _c.Call.Return(run) + return _c +} diff --git a/services/guestauth/pkg/service/guestauth/service.go b/services/guestauth/pkg/service/guestauth/service.go index 5aeba5f52f..e56d343c73 100644 --- a/services/guestauth/pkg/service/guestauth/service.go +++ b/services/guestauth/pkg/service/guestauth/service.go @@ -23,6 +23,15 @@ var ErrAlreadyRedeemed = errors.New("token already redeemed") var ErrShareNotFound = errors.New("share not found") var ErrShareExpired = errors.New("share expired") +// GuestAuth is the domain service used by the transport and event layers. +type GuestAuth interface { + CreateToken(ctx context.Context, shareID string) (*token.Token, error) + Redeem(ctx context.Context, tokenString string) (string, error) + CleanupShare(shareID string) error +} + +var _ GuestAuth = (*GuestAuthService)(nil) + // GuestAuthService contains the business logic shared by guestauth transport services. type GuestAuthService struct { tokenSvc *token.TokenService @@ -47,17 +56,22 @@ func NewGuestAuthService(tokenSvc *token.TokenService, store storage.Storage, op } } -func (s *GuestAuthService) CreateToken(shareID string) (*token.Token, error) { +func (s *GuestAuthService) CreateToken(ctx context.Context, shareID string) (*token.Token, error) { tok, err := s.tokenSvc.Generate(shareID) if err != nil { return nil, err } - // ShareCreated carries no expiration; expiry is checked against the share when the token is redeemed. + share, err := s.getShare(ctx, shareID) + if err != nil { + return nil, err + } + if err := s.store.Add(storage.Record{ ShareID: shareID, ShareIDHash: tok.ShareIDHash, SecretHash: tok.SecretHash, + Expiry: utils.TSToTime(share.GetExpiration()), Redeemed: false, }); err != nil { return nil, err @@ -77,16 +91,14 @@ func (s *GuestAuthService) Redeem(ctx context.Context, tokenString string) (stri return "", err } - sessionToken, err := s.jwtService.Sign(rec.ShareID) - if err != nil { - return "", err - } - if err := s.store.Redeem(rec.ShareIDHash); err != nil { + if errors.Is(err, storage.ErrAlreadyRedeemed) { + return "", ErrAlreadyRedeemed + } return "", err } - return sessionToken, nil + return s.jwtService.Sign(rec.ShareID) } // CleanupShare removes a share's token record from storage. Missing records are ignored. @@ -129,6 +141,20 @@ func (s *GuestAuthService) verifyToken(tokenString string) (storage.Record, erro // validateShare extracts the share information from the gateway and checks its existence and expiration. func (s *GuestAuthService) validateShare(ctx context.Context, shareID string) (*collaboration.Share, error) { + share, err := s.getShare(ctx, shareID) + if err != nil { + return nil, err + } + + if exp := utils.TSToTime(share.GetExpiration()); !exp.IsZero() && exp.Before(time.Now()) { + return nil, ErrShareExpired + } + + return share, nil +} + +// getShare fetches a share from the gateway. +func (s *GuestAuthService) getShare(ctx context.Context, shareID string) (*collaboration.Share, error) { gwc, err := s.gatewaySelector.Next() if err != nil { return nil, err @@ -165,9 +191,5 @@ func (s *GuestAuthService) validateShare(ctx context.Context, shareID string) (* return nil, ErrShareNotFound } - if exp := utils.TSToTime(share.GetExpiration()); !exp.IsZero() && exp.Before(time.Now()) { - return nil, ErrShareExpired - } - return share, nil } diff --git a/services/guestauth/pkg/service/guestauth/service_test.go b/services/guestauth/pkg/service/guestauth/service_test.go index 435d5c3550..244dbdb595 100644 --- a/services/guestauth/pkg/service/guestauth/service_test.go +++ b/services/guestauth/pkg/service/guestauth/service_test.go @@ -11,6 +11,7 @@ import ( "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/jwt" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" + storagemocks "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage/mocks" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" "github.com/opencloud-eu/reva/v2/pkg/utils" @@ -60,15 +61,11 @@ func newToken(t *testing.T) (string, storage.Record) { return tok.String(), rec } -func newStorage(t *testing.T) storage.Storage { - return storage.NewFileStorage(t.TempDir()) -} - func newShareService(t *testing.T, gwc *cs3mocks.GatewayAPIClient) *GuestAuthService { t.Helper() return NewGuestAuthService( token.NewTokenService(), - newStorage(t), + storagemocks.NewStorage(t), GatewaySelector(newGatewayTestSelector(gwc)), ServiceAccount(config.ServiceAccount{ServiceAccountID: "sa-id", ServiceAccountSecret: "sa-secret"}), ) @@ -86,19 +83,37 @@ func newRedeemService(t *testing.T, store storage.Storage, gwc *cs3mocks.Gateway } func TestCreateTokenPersistsRecord(t *testing.T) { - store := newStorage(t) - s := NewGuestAuthService(token.NewTokenService(), store) + store := storagemocks.NewStorage(t) + expiry := time.Date(2027, 1, 2, 3, 4, 5, 0, time.UTC) + gwc := newGatewayMock(&collaboration.GetShareResponse{ + Status: &rpc.Status{Code: rpc.Code_CODE_OK}, + Share: &collaboration.Share{ + Id: &collaboration.ShareId{OpaqueId: testShareID}, + Expiration: utils.TimeToTS(expiry), + }, + }) - tok, err := s.CreateToken(testShareID) + var added storage.Record + store.On("Add", mock.Anything).Run(func(args mock.Arguments) { + added = args.Get(0).(storage.Record) + }).Return(nil) + + s := NewGuestAuthService( + token.NewTokenService(), + store, + GatewaySelector(newGatewayTestSelector(gwc)), + ServiceAccount(config.ServiceAccount{ServiceAccountID: "sa-id", ServiceAccountSecret: "sa-secret"}), + ) + + tok, err := s.CreateToken(context.Background(), testShareID) require.NoError(t, err) - rec, err := store.Get(tok.ShareIDHash) - require.NoError(t, err) - assert.Equal(t, testShareID, rec.ShareID) - assert.Equal(t, tok.ShareIDHash, rec.ShareIDHash) - assert.Equal(t, tok.SecretHash, rec.SecretHash) - assert.True(t, rec.Expiry.IsZero()) - assert.False(t, rec.Redeemed) + store.AssertCalled(t, "Add", mock.Anything) + assert.Equal(t, testShareID, added.ShareID) + assert.Equal(t, tok.ShareIDHash, added.ShareIDHash) + assert.Equal(t, tok.SecretHash, added.SecretHash) + assert.True(t, expiry.Equal(added.Expiry)) + assert.False(t, added.Redeemed) } func TestVerifyToken(t *testing.T) { @@ -115,16 +130,16 @@ func TestVerifyToken(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - store := newStorage(t) + store := storagemocks.NewStorage(t) s := NewGuestAuthService(token.NewTokenService(), store) tok, rec := newToken(t) if tt.expired { rec.Expiry = time.Now().Add(-time.Hour) } - require.NoError(t, store.Add(rec)) if tt.redeemed { - require.NoError(t, store.Redeem(rec.ShareIDHash)) + rec.Redeemed = true } + store.On("Get", rec.ShareIDHash).Return(rec, nil) got, err := s.verifyToken(tok) if tt.wantErr != nil { @@ -196,9 +211,10 @@ func TestValidateShare(t *testing.T) { } func TestRedeem(t *testing.T) { - store := newStorage(t) + store := storagemocks.NewStorage(t) tok, rec := newToken(t) - require.NoError(t, store.Add(rec)) + store.On("Get", rec.ShareIDHash).Return(rec, nil) + store.On("Redeem", rec.ShareIDHash).Return(nil) share := &collaboration.Share{Id: &collaboration.ShareId{OpaqueId: testShareID}} s := newRedeemService(t, store, newGatewayMock(&collaboration.GetShareResponse{ @@ -210,7 +226,21 @@ func TestRedeem(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, sessionToken) - got, err := store.Get(rec.ShareIDHash) - require.NoError(t, err) - assert.True(t, got.Redeemed) + store.AssertCalled(t, "Redeem", rec.ShareIDHash) +} + +func TestRedeemAlreadyRedeemed(t *testing.T) { + store := storagemocks.NewStorage(t) + tok, rec := newToken(t) + store.On("Get", rec.ShareIDHash).Return(rec, nil) + store.On("Redeem", rec.ShareIDHash).Return(storage.ErrAlreadyRedeemed) + + share := &collaboration.Share{Id: &collaboration.ShareId{OpaqueId: testShareID}} + s := newRedeemService(t, store, newGatewayMock(&collaboration.GetShareResponse{ + Status: &rpc.Status{Code: rpc.Code_CODE_OK}, + Share: share, + })) + + _, err := s.Redeem(context.Background(), tok) + assert.ErrorIs(t, err, ErrAlreadyRedeemed) } diff --git a/services/guestauth/pkg/service/storage/file_storage.go b/services/guestauth/pkg/service/storage/file_storage.go index 8f7aa2557f..d8aeb084b5 100644 --- a/services/guestauth/pkg/service/storage/file_storage.go +++ b/services/guestauth/pkg/service/storage/file_storage.go @@ -66,6 +66,10 @@ func (s *FileStorage) Redeem(shareIDHash string) error { return err } + if rec.Redeemed { + return ErrAlreadyRedeemed + } + rec.Redeemed = true return s.add(rec) } diff --git a/services/guestauth/pkg/service/storage/file_storage_test.go b/services/guestauth/pkg/service/storage/file_storage_test.go index 1f9b9e549b..f89f1135d3 100644 --- a/services/guestauth/pkg/service/storage/file_storage_test.go +++ b/services/guestauth/pkg/service/storage/file_storage_test.go @@ -89,6 +89,9 @@ func TestFileStorageRedeem(t *testing.T) { got, err := s.Get(rec.ShareIDHash) require.NoError(t, err) assert.True(t, got.Redeemed) + + err = s.Redeem(rec.ShareIDHash) + assert.ErrorIs(t, err, ErrAlreadyRedeemed) } func TestFileStorageRedeemMissing(t *testing.T) { diff --git a/services/guestauth/pkg/service/storage/mocks/storage.go b/services/guestauth/pkg/service/storage/mocks/storage.go new file mode 100644 index 0000000000..93eda408ce --- /dev/null +++ b/services/guestauth/pkg/service/storage/mocks/storage.go @@ -0,0 +1,250 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" + mock "github.com/stretchr/testify/mock" +) + +// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewStorage(t interface { + mock.TestingT + Cleanup(func()) +}) *Storage { + mock := &Storage{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// Storage is an autogenerated mock type for the Storage type +type Storage struct { + mock.Mock +} + +type Storage_Expecter struct { + mock *mock.Mock +} + +func (_m *Storage) EXPECT() *Storage_Expecter { + return &Storage_Expecter{mock: &_m.Mock} +} + +// Add provides a mock function for the type Storage +func (_mock *Storage) Add(rec storage.Record) error { + ret := _mock.Called(rec) + + if len(ret) == 0 { + panic("no return value specified for Add") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(storage.Record) error); ok { + r0 = returnFunc(rec) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// Storage_Add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Add' +type Storage_Add_Call struct { + *mock.Call +} + +// Add is a helper method to define mock.On call +// - rec storage.Record +func (_e *Storage_Expecter) Add(rec any) *Storage_Add_Call { + return &Storage_Add_Call{Call: _e.mock.On("Add", rec)} +} + +func (_c *Storage_Add_Call) Run(run func(rec storage.Record)) *Storage_Add_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 storage.Record + if args[0] != nil { + arg0 = args[0].(storage.Record) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *Storage_Add_Call) Return(err error) *Storage_Add_Call { + _c.Call.Return(err) + return _c +} + +func (_c *Storage_Add_Call) RunAndReturn(run func(rec storage.Record) error) *Storage_Add_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function for the type Storage +func (_mock *Storage) Get(shareIDHash string) (storage.Record, error) { + ret := _mock.Called(shareIDHash) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 storage.Record + var r1 error + if returnFunc, ok := ret.Get(0).(func(string) (storage.Record, error)); ok { + return returnFunc(shareIDHash) + } + if returnFunc, ok := ret.Get(0).(func(string) storage.Record); ok { + r0 = returnFunc(shareIDHash) + } else { + r0 = ret.Get(0).(storage.Record) + } + if returnFunc, ok := ret.Get(1).(func(string) error); ok { + r1 = returnFunc(shareIDHash) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// Storage_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type Storage_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - shareIDHash string +func (_e *Storage_Expecter) Get(shareIDHash any) *Storage_Get_Call { + return &Storage_Get_Call{Call: _e.mock.On("Get", shareIDHash)} +} + +func (_c *Storage_Get_Call) Run(run func(shareIDHash string)) *Storage_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *Storage_Get_Call) Return(record storage.Record, err error) *Storage_Get_Call { + _c.Call.Return(record, err) + return _c +} + +func (_c *Storage_Get_Call) RunAndReturn(run func(shareIDHash string) (storage.Record, error)) *Storage_Get_Call { + _c.Call.Return(run) + return _c +} + +// Redeem provides a mock function for the type Storage +func (_mock *Storage) Redeem(shareIDHash string) error { + ret := _mock.Called(shareIDHash) + + if len(ret) == 0 { + panic("no return value specified for Redeem") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string) error); ok { + r0 = returnFunc(shareIDHash) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// Storage_Redeem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Redeem' +type Storage_Redeem_Call struct { + *mock.Call +} + +// Redeem is a helper method to define mock.On call +// - shareIDHash string +func (_e *Storage_Expecter) Redeem(shareIDHash any) *Storage_Redeem_Call { + return &Storage_Redeem_Call{Call: _e.mock.On("Redeem", shareIDHash)} +} + +func (_c *Storage_Redeem_Call) Run(run func(shareIDHash string)) *Storage_Redeem_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *Storage_Redeem_Call) Return(err error) *Storage_Redeem_Call { + _c.Call.Return(err) + return _c +} + +func (_c *Storage_Redeem_Call) RunAndReturn(run func(shareIDHash string) error) *Storage_Redeem_Call { + _c.Call.Return(run) + return _c +} + +// Remove provides a mock function for the type Storage +func (_mock *Storage) Remove(shareIDHash string) error { + ret := _mock.Called(shareIDHash) + + if len(ret) == 0 { + panic("no return value specified for Remove") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string) error); ok { + r0 = returnFunc(shareIDHash) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// Storage_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove' +type Storage_Remove_Call struct { + *mock.Call +} + +// Remove is a helper method to define mock.On call +// - shareIDHash string +func (_e *Storage_Expecter) Remove(shareIDHash any) *Storage_Remove_Call { + return &Storage_Remove_Call{Call: _e.mock.On("Remove", shareIDHash)} +} + +func (_c *Storage_Remove_Call) Run(run func(shareIDHash string)) *Storage_Remove_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *Storage_Remove_Call) Return(err error) *Storage_Remove_Call { + _c.Call.Return(err) + return _c +} + +func (_c *Storage_Remove_Call) RunAndReturn(run func(shareIDHash string) error) *Storage_Remove_Call { + _c.Call.Return(run) + return _c +} diff --git a/services/guestauth/pkg/service/storage/storage.go b/services/guestauth/pkg/service/storage/storage.go index 91389d067e..a92ecf2dec 100644 --- a/services/guestauth/pkg/service/storage/storage.go +++ b/services/guestauth/pkg/service/storage/storage.go @@ -5,8 +5,8 @@ import ( "time" ) -// ErrNotFound is returned when a record does not exist in the storage. var ErrNotFound = errors.New("record not found") +var ErrAlreadyRedeemed = errors.New("token already redeemed") // Record holds the data persisted for a guest share token. type Record struct { @@ -17,8 +17,6 @@ type Record struct { Redeemed bool `json:"redeemed"` } -// Storage is the interface for persisting token records. Implementations need -// to be safe for concurrent use. type Storage interface { Add(rec Record) error Get(shareIDHash string) (Record, error) From 45bc705a74bcab2a862ff6669c679215018e6845 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Mon, 28 Sep 2026 10:45:09 +0200 Subject: [PATCH 13/32] feat(guestauth): cookie set always to true due to codacy mention --- services/guestauth/pkg/config/config.go | 5 ++--- services/guestauth/pkg/server/http/redeem.go | 2 +- services/guestauth/pkg/server/http/redeem_test.go | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/services/guestauth/pkg/config/config.go b/services/guestauth/pkg/config/config.go index 2136394977..944ab85475 100644 --- a/services/guestauth/pkg/config/config.go +++ b/services/guestauth/pkg/config/config.go @@ -82,7 +82,6 @@ type TokenManager struct { // JWT defines the configuration for guest session tokens. type JWT struct { - CookieName string `yaml:"cookie_name" env:"GUESTAUTH_JWT_COOKIE_NAME" desc:"The name of the session cookie set when a guest token is redeemed." introductionVersion:"1.0.0"` - CookieSecure bool `yaml:"cookie_secure" env:"GUESTAUTH_JWT_COOKIE_SECURE" desc:"Whether the session cookie should be flagged as secure (only sent over HTTPS)." introductionVersion:"1.0.0"` - TTL time.Duration `yaml:"ttl" env:"GUESTAUTH_JWT_TTL" desc:"The lifetime of a redeemed guest session token." introductionVersion:"1.0.0"` + CookieName string `yaml:"cookie_name" env:"GUESTAUTH_JWT_COOKIE_NAME" desc:"The name of the session cookie set when a guest token is redeemed." introductionVersion:"1.0.0"` + TTL time.Duration `yaml:"ttl" env:"GUESTAUTH_JWT_TTL" desc:"The lifetime of a redeemed guest session token." introductionVersion:"1.0.0"` } diff --git a/services/guestauth/pkg/server/http/redeem.go b/services/guestauth/pkg/server/http/redeem.go index 192f97ae47..ce88d78530 100644 --- a/services/guestauth/pkg/server/http/redeem.go +++ b/services/guestauth/pkg/server/http/redeem.go @@ -61,7 +61,7 @@ func RedeemHandler(log log.Logger, s guestauth.GuestAuth, cfg *config.Config) fu Value: sessionToken, Path: "/", HttpOnly: true, - Secure: cfg.JWT.CookieSecure, + Secure: true, SameSite: http.SameSiteLaxMode, MaxAge: int(cfg.JWT.TTL.Seconds()), }) diff --git a/services/guestauth/pkg/server/http/redeem_test.go b/services/guestauth/pkg/server/http/redeem_test.go index 02987abcd7..1a05dfee20 100644 --- a/services/guestauth/pkg/server/http/redeem_test.go +++ b/services/guestauth/pkg/server/http/redeem_test.go @@ -24,7 +24,6 @@ func newRedeemHandler(t *testing.T, svc guestauth.GuestAuth) http.HandlerFunc { cfg := &config.Config{ JWT: config.JWT{ CookieName: "oc_guest_session", - CookieSecure: true, TTL: time.Hour, }, } From d359a7da1fdb4951015ae6471527da05512199b3 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Mon, 28 Sep 2026 10:50:41 +0200 Subject: [PATCH 14/32] feat(guestauth): removed uneeded config field --- services/guestauth/pkg/config/defaults/defaultconfig.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/guestauth/pkg/config/defaults/defaultconfig.go b/services/guestauth/pkg/config/defaults/defaultconfig.go index 2d217bb298..fe2737e2ab 100644 --- a/services/guestauth/pkg/config/defaults/defaultconfig.go +++ b/services/guestauth/pkg/config/defaults/defaultconfig.go @@ -52,9 +52,8 @@ func DefaultConfig() *config.Config { RootDirectory: path.Join(defaults.BaseDataPath(), "guestauth"), }, JWT: config.JWT{ - CookieName: "oc_guest_session", - CookieSecure: true, - TTL: 24 * time.Hour, + CookieName: "oc_guest_session", + TTL: 24 * time.Hour, }, } } From fdf52e4eaff332b86a7b23877d40ac05c0c32366 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Mon, 28 Sep 2026 11:09:03 +0200 Subject: [PATCH 15/32] feat(guestauth): upd service readme --- services/guestauth/README.md | 50 +++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/services/guestauth/README.md b/services/guestauth/README.md index 52909479e1..8360c48ae1 100644 --- a/services/guestauth/README.md +++ b/services/guestauth/README.md @@ -1,17 +1,53 @@ # Guestauth -The `guestauth` service is responsible for creating and validating guest login tokens and exchanging them for a session cookie. +The `guestauth` service gives guest users access to a share without a full +OpenCloud account. When a share is created for a user of type +`USER_TYPE_GUEST`, the service issues a one-time invitation token; redeeming +that token exchanges it for a signed session cookie that authenticates the +guest. -It is part of the default service set. It does not need to be explicitly enabled with `OC_ADD_RUN_SERVICES`. +It is part of the default service set and does not need to be enabled with +`OC_ADD_RUN_SERVICES`. + +## Overview + +- **Consumes** the share lifecycle events `ShareCreated`, `ShareRemoved` and + `ShareExpired`. +- **Publishes** the `GuestTokenCreated` event carrying the invitation token, + so the invitation can be delivered to the guest. +- Exposes an unauthenticated endpoint that redeems the token and sets a + session cookie. +- Stores only hashes of the token and deletes the stored record when the share + is removed or expires. ## Token lifecycle -- When a guest share is created (`ShareCreated` event), the service generates a secure random token, persists it and emits a `GuestTokenCreated` event. -- When a guest share is removed or expires (`ShareRemoved` / `ShareExpired` events),the service cleans up the stored token. -- The token can be redeemed via `POST /graph/v1beta1/guestInvitations/redeem` to exchange it for a session cookie. +1. **Issue** — on the consumed `ShareCreated` event, where the grantee is a + guest, the service generates a random secret and stores a record keyed by + the hash of the share id. It then publishes the `GuestTokenCreated` event + with the token. +2. **Redeem** — the guest posts the token to + `POST /graph/v1beta1/guestInvitations/redeem`. The service validates the + token and the share, marks the token as used and returns a signed JWT + session token in a cookie. Tokens are single-use. +3. **Cleanup** — on the consumed `ShareRemoved` or `ShareExpired` event, the + stored record is deleted. ## Configuration -The service can be configured via environment variables (prefix `GUESTAUTH_*`)or a `guestauth.yaml` configuration file. +The service is configured via `GUESTAUTH_*` environment variables or a +`guestauth.yaml` file. -To run the service without consuming events, set `GUESTAUTH_EVENTS_DISABLED=true`. To run it without the HTTP service, set `GUESTAUTH_HTTP_DISABLED=true`. \ No newline at end of file +To run only the HTTP part, set `GUESTAUTH_EVENTS_DISABLED=true`. To run only +the event consumer, set `GUESTAUTH_HTTP_DISABLED=true`. + +Relevant options: + +- `GUESTAUTH_JWT_SECRET` — secret used to sign session tokens. +- `GUESTAUTH_JWT_COOKIE_NAME`, `GUESTAUTH_JWT_TTL` — session cookie name and + lifetime. +- `GUESTAUTH_TOKENS_STORAGE_ROOT` — where invitation token records are stored. +- `GUESTAUTH_SERVICE_ACCOUNT_ID`, `GUESTAUTH_SERVICE_ACCOUNT_SECRET` — service + account used to query the gateway for share metadata. +- `GUESTAUTH_NUM_CONSUMERS` — number of concurrent event consumers. +- `OC_REVA_GATEWAY` — CS3 gateway used to look up shares. From 2106f60fd897187c5e5a29eebcc99243aba23eb3 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Tue, 29 Sep 2026 09:49:39 +0200 Subject: [PATCH 16/32] feat(guestauth): upd version tags for env vars --- services/guestauth/pkg/config/config.go | 48 +++++++++---------- services/guestauth/pkg/config/debug.go | 8 ++-- .../guestauth/pkg/server/http/redeem_test.go | 4 +- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/services/guestauth/pkg/config/config.go b/services/guestauth/pkg/config/config.go index 944ab85475..ae795fef94 100644 --- a/services/guestauth/pkg/config/config.go +++ b/services/guestauth/pkg/config/config.go @@ -13,13 +13,13 @@ type Config struct { Service Service `yaml:"-"` - LogLevel string `yaml:"loglevel" env:"OC_LOG_LEVEL;GUESTAUTH_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"1.0.0"` + LogLevel string `yaml:"loglevel" env:"OC_LOG_LEVEL;GUESTAUTH_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"%%NEXT%%"` Debug Debug `yaml:"debug"` Events Events `yaml:"events"` - RevaGateway string `yaml:"reva_gateway" env:"OC_REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata" introductionVersion:"1.0.0"` + RevaGateway string `yaml:"reva_gateway" env:"OC_REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata" introductionVersion:"%%NEXT%%"` GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"` HTTP HTTP `yaml:"http"` @@ -29,59 +29,59 @@ type Config struct { ServiceAccount ServiceAccount `yaml:"service_account"` - NumConsumers int `yaml:"num_consumers" env:"GUESTAUTH_NUM_CONSUMERS" desc:"The amount of concurrent event consumers to start. Event consumers are used for processing events. Multiple consumers increase parallelisation, but will also increase CPU and memory demands." introductionVersion:"1.0.0"` + NumConsumers int `yaml:"num_consumers" env:"GUESTAUTH_NUM_CONSUMERS" desc:"The amount of concurrent event consumers to start. Event consumers are used for processing events. Multiple consumers increase parallelisation, but will also increase CPU and memory demands." introductionVersion:"%%NEXT%%"` Context context.Context `yaml:"-"` } // Events combines the configuration options for the event bus. type Events struct { - Disabled bool `yaml:"disabled" env:"GUESTAUTH_EVENTS_DISABLED" desc:"Disables listening for events. Set this to true if the service should only handle HTTP requests." introductionVersion:"1.0.0"` - Endpoint string `yaml:"endpoint" env:"OC_EVENTS_ENDPOINT" desc:"The address of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture." introductionVersion:"1.0.0"` - Cluster string `yaml:"cluster" env:"OC_EVENTS_CLUSTER" desc:"The clusterID of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture. Mandatory when using NATS as event system." introductionVersion:"1.0.0"` - TLSInsecure bool `yaml:"tls_insecure" env:"OC_INSECURE;OC_EVENTS_TLS_INSECURE" desc:"Whether to verify the server TLS certificates." introductionVersion:"1.0.0"` - TLSRootCACertificate string `yaml:"tls_root_ca_certificate" env:"OC_EVENTS_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided GUESTAUTH_EVENTS_TLS_INSECURE will be seen as false." introductionVersion:"1.0.0"` - EnableTLS bool `yaml:"enable_tls" env:"OC_EVENTS_ENABLE_TLS" desc:"Enable TLS for the connection to the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"1.0.0"` - AuthUsername string `yaml:"username" env:"OC_EVENTS_AUTH_USERNAME" desc:"The username to authenticate with the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"1.0.0"` - AuthPassword string `yaml:"password" env:"OC_EVENTS_AUTH_PASSWORD" desc:"The password to authenticate with the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"1.0.0"` + Disabled bool `yaml:"disabled" env:"GUESTAUTH_EVENTS_DISABLED" desc:"Disables listening for events. Set this to true if the service should only handle HTTP requests." introductionVersion:"%%NEXT%%"` + Endpoint string `yaml:"endpoint" env:"OC_EVENTS_ENDPOINT" desc:"The address of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture." introductionVersion:"%%NEXT%%"` + Cluster string `yaml:"cluster" env:"OC_EVENTS_CLUSTER" desc:"The clusterID of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture. Mandatory when using NATS as event system." introductionVersion:"%%NEXT%%"` + TLSInsecure bool `yaml:"tls_insecure" env:"OC_INSECURE;OC_EVENTS_TLS_INSECURE" desc:"Whether to verify the server TLS certificates." introductionVersion:"%%NEXT%%"` + TLSRootCACertificate string `yaml:"tls_root_ca_certificate" env:"OC_EVENTS_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided GUESTAUTH_EVENTS_TLS_INSECURE will be seen as false." introductionVersion:"%%NEXT%%"` + EnableTLS bool `yaml:"enable_tls" env:"OC_EVENTS_ENABLE_TLS" desc:"Enable TLS for the connection to the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"%%NEXT%%"` + AuthUsername string `yaml:"username" env:"OC_EVENTS_AUTH_USERNAME" desc:"The username to authenticate with the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"%%NEXT%%"` + AuthPassword string `yaml:"password" env:"OC_EVENTS_AUTH_PASSWORD" desc:"The password to authenticate with the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"%%NEXT%%"` } // ServiceAccount is the configuration for the used service account type ServiceAccount struct { - ServiceAccountID string `yaml:"service_account_id" env:"OC_SERVICE_ACCOUNT_ID;GUESTAUTH_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details." introductionVersion:"1.0.0"` - ServiceAccountSecret string `yaml:"service_account_secret" env:"OC_SERVICE_ACCOUNT_SECRET;GUESTAUTH_SERVICE_ACCOUNT_SECRET" desc:"The service account secret." introductionVersion:"1.0.0"` + ServiceAccountID string `yaml:"service_account_id" env:"OC_SERVICE_ACCOUNT_ID;GUESTAUTH_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details." introductionVersion:"%%NEXT%%"` + ServiceAccountSecret string `yaml:"service_account_secret" env:"OC_SERVICE_ACCOUNT_SECRET;GUESTAUTH_SERVICE_ACCOUNT_SECRET" desc:"The service account secret." introductionVersion:"%%NEXT%%"` } // CORS defines the available cors configuration. type CORS struct { - AllowedOrigins []string `yaml:"allow_origins" env:"OC_CORS_ALLOW_ORIGINS;GUESTAUTH_CORS_ALLOW_ORIGINS" desc:"A 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. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"` - AllowedMethods []string `yaml:"allow_methods" env:"OC_CORS_ALLOW_METHODS;GUESTAUTH_CORS_ALLOW_METHODS" desc:"A 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. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"` - AllowedHeaders []string `yaml:"allow_headers" env:"OC_CORS_ALLOW_HEADERS;GUESTAUTH_CORS_ALLOW_HEADERS" desc:"A 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. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"` - AllowCredentials bool `yaml:"allow_credentials" env:"OC_CORS_ALLOW_CREDENTIALS;GUESTAUTH_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." introductionVersion:"1.0.0"` + AllowedOrigins []string `yaml:"allow_origins" env:"OC_CORS_ALLOW_ORIGINS;GUESTAUTH_CORS_ALLOW_ORIGINS" desc:"A 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. See the Environment Variable Types description for more details." introductionVersion:"%%NEXT%%"` + AllowedMethods []string `yaml:"allow_methods" env:"OC_CORS_ALLOW_METHODS;GUESTAUTH_CORS_ALLOW_METHODS" desc:"A 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. See the Environment Variable Types description for more details." introductionVersion:"%%NEXT%%"` + AllowedHeaders []string `yaml:"allow_headers" env:"OC_CORS_ALLOW_HEADERS;GUESTAUTH_CORS_ALLOW_HEADERS" desc:"A 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. See the Environment Variable Types description for more details." introductionVersion:"%%NEXT%%"` + AllowCredentials bool `yaml:"allow_credentials" env:"OC_CORS_ALLOW_CREDENTIALS;GUESTAUTH_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." introductionVersion:"%%NEXT%%"` } // HTTP defines the available http configuration. type HTTP struct { - Disabled bool `yaml:"disabled" env:"GUESTAUTH_HTTP_DISABLED" desc:"Disables the HTTP service. Set this to true if the service should only handle events." introductionVersion:"1.0.0"` - Addr string `yaml:"addr" env:"GUESTAUTH_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"1.0.0"` + Disabled bool `yaml:"disabled" env:"GUESTAUTH_HTTP_DISABLED" desc:"Disables the HTTP service. Set this to true if the service should only handle events." introductionVersion:"%%NEXT%%"` + Addr string `yaml:"addr" env:"GUESTAUTH_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"%%NEXT%%"` Namespace string `yaml:"-"` - Root string `yaml:"root" env:"GUESTAUTH_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"1.0.0"` + Root string `yaml:"root" env:"GUESTAUTH_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"%%NEXT%%"` CORS CORS `yaml:"cors"` TLS shared.HTTPServiceTLS `yaml:"tls"` } // Storage defines the configuration for the token storage. type Storage struct { - RootDirectory string `yaml:"root_directory" env:"GUESTAUTH_TOKENS_STORAGE_ROOT" desc:"The directory where the guest share tokens are stored. If not defined, the root directory derives from $OC_BASE_DATA_PATH/guestauth." introductionVersion:"1.0.0"` + RootDirectory string `yaml:"root_directory" env:"GUESTAUTH_TOKENS_STORAGE_ROOT" desc:"The directory where the guest share tokens are stored. If not defined, the root directory derives from $OC_BASE_DATA_PATH/guestauth." introductionVersion:"%%NEXT%%"` } // TokenManager is the config for using the reva token manager type TokenManager struct { - JWTSecret string `yaml:"jwt_secret" env:"GUESTAUTH_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"1.0.0"` + JWTSecret string `yaml:"jwt_secret" env:"GUESTAUTH_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"%%NEXT%%"` } // JWT defines the configuration for guest session tokens. type JWT struct { - CookieName string `yaml:"cookie_name" env:"GUESTAUTH_JWT_COOKIE_NAME" desc:"The name of the session cookie set when a guest token is redeemed." introductionVersion:"1.0.0"` - TTL time.Duration `yaml:"ttl" env:"GUESTAUTH_JWT_TTL" desc:"The lifetime of a redeemed guest session token." introductionVersion:"1.0.0"` + CookieName string `yaml:"cookie_name" env:"GUESTAUTH_JWT_COOKIE_NAME" desc:"The name of the session cookie set when a guest token is redeemed." introductionVersion:"%%NEXT%%"` + TTL time.Duration `yaml:"ttl" env:"GUESTAUTH_JWT_TTL" desc:"The lifetime of a redeemed guest session token." introductionVersion:"%%NEXT%%"` } diff --git a/services/guestauth/pkg/config/debug.go b/services/guestauth/pkg/config/debug.go index e7da076c0e..4912b1825c 100644 --- a/services/guestauth/pkg/config/debug.go +++ b/services/guestauth/pkg/config/debug.go @@ -2,8 +2,8 @@ package config // Debug defines the available debug configuration. type Debug struct { - Addr string `yaml:"addr" env:"GUESTAUTH_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:"GUESTAUTH_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"1.0.0"` - Pprof bool `yaml:"pprof" env:"GUESTAUTH_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"1.0.0"` - Zpages bool `yaml:"zpages" env:"GUESTAUTH_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"1.0.0"` + Addr string `yaml:"addr" env:"GUESTAUTH_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"%%NEXT%%"` + Token string `yaml:"token" env:"GUESTAUTH_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"%%NEXT%%"` + Pprof bool `yaml:"pprof" env:"GUESTAUTH_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"%%NEXT%%"` + Zpages bool `yaml:"zpages" env:"GUESTAUTH_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"%%NEXT%%"` } diff --git a/services/guestauth/pkg/server/http/redeem_test.go b/services/guestauth/pkg/server/http/redeem_test.go index 1a05dfee20..d3fda98281 100644 --- a/services/guestauth/pkg/server/http/redeem_test.go +++ b/services/guestauth/pkg/server/http/redeem_test.go @@ -23,8 +23,8 @@ func newRedeemHandler(t *testing.T, svc guestauth.GuestAuth) http.HandlerFunc { t.Helper() cfg := &config.Config{ JWT: config.JWT{ - CookieName: "oc_guest_session", - TTL: time.Hour, + CookieName: "oc_guest_session", + TTL: time.Hour, }, } return RedeemHandler(log.NopLogger(), svc, cfg) From b4aa47c029f1bba99787f7e2ca88426de0c52b76 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Tue, 29 Sep 2026 11:29:17 +0200 Subject: [PATCH 17/32] feat(guestauth): renamed Storage interface and dependencies to Manager --- services/guestauth/.mockery.yaml | 2 +- services/guestauth/pkg/command/server.go | 2 +- .../pkg/service/guestauth/service.go | 4 +- .../pkg/service/guestauth/service_test.go | 12 +-- .../{file_storage.go => file_manager.go} | 20 ++--- ...e_storage_test.go => file_manager_test.go} | 28 +++--- .../storage/mocks/{storage.go => manager.go} | 90 +++++++++---------- .../guestauth/pkg/service/storage/storage.go | 2 +- 8 files changed, 80 insertions(+), 80 deletions(-) rename services/guestauth/pkg/service/storage/{file_storage.go => file_manager.go} (75%) rename services/guestauth/pkg/service/storage/{file_storage_test.go => file_manager_test.go} (78%) rename services/guestauth/pkg/service/storage/mocks/{storage.go => manager.go} (55%) diff --git a/services/guestauth/.mockery.yaml b/services/guestauth/.mockery.yaml index 8b68d395af..53f85a5896 100644 --- a/services/guestauth/.mockery.yaml +++ b/services/guestauth/.mockery.yaml @@ -11,4 +11,4 @@ packages: GuestAuth: {} github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage: interfaces: - Storage: {} + Manager: {} diff --git a/services/guestauth/pkg/command/server.go b/services/guestauth/pkg/command/server.go index feab1ff39d..c8fc5cccba 100644 --- a/services/guestauth/pkg/command/server.go +++ b/services/guestauth/pkg/command/server.go @@ -73,7 +73,7 @@ func Server(cfg *config.Config) *cobra.Command { mtrcs.BuildInfo.WithLabelValues(version.GetString()).Set(1) tokenSvc := token.NewTokenService() - store := storage.NewFileStorage(cfg.Storage.RootDirectory) + store := storage.NewFileManager(cfg.Storage.RootDirectory) jwtService := jwt.NewJwtService(cfg.TokenManager.JWTSecret, cfg.JWT.TTL) guestAuth := guestauth.NewGuestAuthService(tokenSvc, store, diff --git a/services/guestauth/pkg/service/guestauth/service.go b/services/guestauth/pkg/service/guestauth/service.go index e56d343c73..7bd26a2f61 100644 --- a/services/guestauth/pkg/service/guestauth/service.go +++ b/services/guestauth/pkg/service/guestauth/service.go @@ -35,13 +35,13 @@ var _ GuestAuth = (*GuestAuthService)(nil) // GuestAuthService contains the business logic shared by guestauth transport services. type GuestAuthService struct { tokenSvc *token.TokenService - store storage.Storage + store storage.Manager gatewaySelector pool.Selectable[gateway.GatewayAPIClient] serviceAccount config.ServiceAccount jwtService *jwt.JwtService } -func NewGuestAuthService(tokenSvc *token.TokenService, store storage.Storage, opts ...Option) *GuestAuthService { +func NewGuestAuthService(tokenSvc *token.TokenService, store storage.Manager, opts ...Option) *GuestAuthService { o := &Options{} for _, opt := range opts { opt(o) diff --git a/services/guestauth/pkg/service/guestauth/service_test.go b/services/guestauth/pkg/service/guestauth/service_test.go index 244dbdb595..9ec8868850 100644 --- a/services/guestauth/pkg/service/guestauth/service_test.go +++ b/services/guestauth/pkg/service/guestauth/service_test.go @@ -65,13 +65,13 @@ func newShareService(t *testing.T, gwc *cs3mocks.GatewayAPIClient) *GuestAuthSer t.Helper() return NewGuestAuthService( token.NewTokenService(), - storagemocks.NewStorage(t), + storagemocks.NewManager(t), GatewaySelector(newGatewayTestSelector(gwc)), ServiceAccount(config.ServiceAccount{ServiceAccountID: "sa-id", ServiceAccountSecret: "sa-secret"}), ) } -func newRedeemService(t *testing.T, store storage.Storage, gwc *cs3mocks.GatewayAPIClient) *GuestAuthService { +func newRedeemService(t *testing.T, store storage.Manager, gwc *cs3mocks.GatewayAPIClient) *GuestAuthService { t.Helper() return NewGuestAuthService( token.NewTokenService(), @@ -83,7 +83,7 @@ func newRedeemService(t *testing.T, store storage.Storage, gwc *cs3mocks.Gateway } func TestCreateTokenPersistsRecord(t *testing.T) { - store := storagemocks.NewStorage(t) + store := storagemocks.NewManager(t) expiry := time.Date(2027, 1, 2, 3, 4, 5, 0, time.UTC) gwc := newGatewayMock(&collaboration.GetShareResponse{ Status: &rpc.Status{Code: rpc.Code_CODE_OK}, @@ -130,7 +130,7 @@ func TestVerifyToken(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - store := storagemocks.NewStorage(t) + store := storagemocks.NewManager(t) s := NewGuestAuthService(token.NewTokenService(), store) tok, rec := newToken(t) if tt.expired { @@ -211,7 +211,7 @@ func TestValidateShare(t *testing.T) { } func TestRedeem(t *testing.T) { - store := storagemocks.NewStorage(t) + store := storagemocks.NewManager(t) tok, rec := newToken(t) store.On("Get", rec.ShareIDHash).Return(rec, nil) store.On("Redeem", rec.ShareIDHash).Return(nil) @@ -230,7 +230,7 @@ func TestRedeem(t *testing.T) { } func TestRedeemAlreadyRedeemed(t *testing.T) { - store := storagemocks.NewStorage(t) + store := storagemocks.NewManager(t) tok, rec := newToken(t) store.On("Get", rec.ShareIDHash).Return(rec, nil) store.On("Redeem", rec.ShareIDHash).Return(storage.ErrAlreadyRedeemed) diff --git a/services/guestauth/pkg/service/storage/file_storage.go b/services/guestauth/pkg/service/storage/file_manager.go similarity index 75% rename from services/guestauth/pkg/service/storage/file_storage.go rename to services/guestauth/pkg/service/storage/file_manager.go index d8aeb084b5..2324036302 100644 --- a/services/guestauth/pkg/service/storage/file_storage.go +++ b/services/guestauth/pkg/service/storage/file_manager.go @@ -12,13 +12,13 @@ import ( "github.com/google/renameio/v2" ) -func NewFileStorage(root string) *FileStorage { - return &FileStorage{ +func NewFileManager(root string) *FileManager { + return &FileManager{ root: root, } } -type FileStorage struct { +type FileManager struct { root string mu sync.Mutex } @@ -26,7 +26,7 @@ type FileStorage struct { const dirPerm = 0700 const filePerm = 0600 -func (s *FileStorage) Add(rec Record) error { +func (s *FileManager) Add(rec Record) error { s.mu.Lock() defer s.mu.Unlock() @@ -34,11 +34,11 @@ func (s *FileStorage) Add(rec Record) error { } // Get returns the record for the given share id hash. -func (s *FileStorage) Get(shareIDHash string) (Record, error) { +func (s *FileManager) Get(shareIDHash string) (Record, error) { return s.get(shareIDHash) } -func (s *FileStorage) Remove(shareIDHash string) error { +func (s *FileManager) Remove(shareIDHash string) error { s.mu.Lock() defer s.mu.Unlock() @@ -57,7 +57,7 @@ func (s *FileStorage) Remove(shareIDHash string) error { return nil } -func (s *FileStorage) Redeem(shareIDHash string) error { +func (s *FileManager) Redeem(shareIDHash string) error { s.mu.Lock() defer s.mu.Unlock() @@ -74,7 +74,7 @@ func (s *FileStorage) Redeem(shareIDHash string) error { return s.add(rec) } -func (s *FileStorage) add(rec Record) error { +func (s *FileManager) add(rec Record) error { data, err := json.Marshal(rec) if err != nil { return err @@ -89,7 +89,7 @@ func (s *FileStorage) add(rec Record) error { return renameio.WriteFile(p, data, filePerm) } -func (s *FileStorage) get(shareIDHash string) (Record, error) { +func (s *FileManager) get(shareIDHash string) (Record, error) { data, err := os.ReadFile(s.path(shareIDHash)) if err != nil { if errors.Is(err, fs.ErrNotExist) { @@ -106,6 +106,6 @@ func (s *FileStorage) get(shareIDHash string) (Record, error) { return rec, nil } -func (s *FileStorage) path(shareIDHash string) string { +func (s *FileManager) path(shareIDHash string) string { return filepath.Join(s.root, shareIDHash[:2], shareIDHash[2:4], shareIDHash[4:]+".json") } diff --git a/services/guestauth/pkg/service/storage/file_storage_test.go b/services/guestauth/pkg/service/storage/file_manager_test.go similarity index 78% rename from services/guestauth/pkg/service/storage/file_storage_test.go rename to services/guestauth/pkg/service/storage/file_manager_test.go index f89f1135d3..3b2418d27d 100644 --- a/services/guestauth/pkg/service/storage/file_storage_test.go +++ b/services/guestauth/pkg/service/storage/file_manager_test.go @@ -21,9 +21,9 @@ func newRecord(shareID string) Record { } } -func TestFileStorageAddGet(t *testing.T) { +func TestFileManagerAddGet(t *testing.T) { dir := t.TempDir() - s := NewFileStorage(dir) + s := NewFileManager(dir) rec := newRecord("e0123456-7890-abcd-ef01-234567890abc") require.NoError(t, s.Add(rec)) @@ -33,17 +33,17 @@ func TestFileStorageAddGet(t *testing.T) { assert.Equal(t, rec, got) } -func TestFileStorageGetMissing(t *testing.T) { +func TestFileManagerGetMissing(t *testing.T) { dir := t.TempDir() - s := NewFileStorage(dir) + s := NewFileManager(dir) _, err := s.Get("doesnotexist") assert.ErrorIs(t, err, ErrNotFound) } -func TestFileStorageAddOverwrites(t *testing.T) { +func TestFileManagerAddOverwrites(t *testing.T) { dir := t.TempDir() - s := NewFileStorage(dir) + s := NewFileManager(dir) rec := newRecord("e0123456-7890-abcd-ef01-234567890abc") require.NoError(t, s.Add(rec)) @@ -56,9 +56,9 @@ func TestFileStorageAddOverwrites(t *testing.T) { assert.Equal(t, "other", got.SecretHash) } -func TestFileStorageRemove(t *testing.T) { +func TestFileManagerRemove(t *testing.T) { dir := t.TempDir() - s := NewFileStorage(dir) + s := NewFileManager(dir) rec := newRecord("e0123456-7890-abcd-ef01-234567890abc") require.NoError(t, s.Add(rec)) @@ -69,17 +69,17 @@ func TestFileStorageRemove(t *testing.T) { assert.ErrorIs(t, err, ErrNotFound) } -func TestFileStorageRemoveMissing(t *testing.T) { +func TestFileManagerRemoveMissing(t *testing.T) { dir := t.TempDir() - s := NewFileStorage(dir) + s := NewFileManager(dir) err := s.Remove("doesnotexist") assert.ErrorIs(t, err, ErrNotFound) } -func TestFileStorageRedeem(t *testing.T) { +func TestFileManagerRedeem(t *testing.T) { dir := t.TempDir() - s := NewFileStorage(dir) + s := NewFileManager(dir) rec := newRecord("e0123456-7890-abcd-ef01-234567890abc") require.NoError(t, s.Add(rec)) @@ -94,9 +94,9 @@ func TestFileStorageRedeem(t *testing.T) { assert.ErrorIs(t, err, ErrAlreadyRedeemed) } -func TestFileStorageRedeemMissing(t *testing.T) { +func TestFileManagerRedeemMissing(t *testing.T) { dir := t.TempDir() - s := NewFileStorage(dir) + s := NewFileManager(dir) err := s.Redeem("doesnotexist") assert.ErrorIs(t, err, ErrNotFound) diff --git a/services/guestauth/pkg/service/storage/mocks/storage.go b/services/guestauth/pkg/service/storage/mocks/manager.go similarity index 55% rename from services/guestauth/pkg/service/storage/mocks/storage.go rename to services/guestauth/pkg/service/storage/mocks/manager.go index 93eda408ce..3f49760c3a 100644 --- a/services/guestauth/pkg/service/storage/mocks/storage.go +++ b/services/guestauth/pkg/service/storage/mocks/manager.go @@ -9,13 +9,13 @@ import ( mock "github.com/stretchr/testify/mock" ) -// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func NewStorage(t interface { +func NewManager(t interface { mock.TestingT Cleanup(func()) -}) *Storage { - mock := &Storage{} +}) *Manager { + mock := &Manager{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) @@ -23,21 +23,21 @@ func NewStorage(t interface { return mock } -// Storage is an autogenerated mock type for the Storage type -type Storage struct { +// Manager is an autogenerated mock type for the Manager type +type Manager struct { mock.Mock } -type Storage_Expecter struct { +type Manager_Expecter struct { mock *mock.Mock } -func (_m *Storage) EXPECT() *Storage_Expecter { - return &Storage_Expecter{mock: &_m.Mock} +func (_m *Manager) EXPECT() *Manager_Expecter { + return &Manager_Expecter{mock: &_m.Mock} } -// Add provides a mock function for the type Storage -func (_mock *Storage) Add(rec storage.Record) error { +// Add provides a mock function for the type Manager +func (_mock *Manager) Add(rec storage.Record) error { ret := _mock.Called(rec) if len(ret) == 0 { @@ -53,18 +53,18 @@ func (_mock *Storage) Add(rec storage.Record) error { return r0 } -// Storage_Add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Add' -type Storage_Add_Call struct { +// Manager_Add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Add' +type Manager_Add_Call struct { *mock.Call } // Add is a helper method to define mock.On call // - rec storage.Record -func (_e *Storage_Expecter) Add(rec any) *Storage_Add_Call { - return &Storage_Add_Call{Call: _e.mock.On("Add", rec)} +func (_e *Manager_Expecter) Add(rec any) *Manager_Add_Call { + return &Manager_Add_Call{Call: _e.mock.On("Add", rec)} } -func (_c *Storage_Add_Call) Run(run func(rec storage.Record)) *Storage_Add_Call { +func (_c *Manager_Add_Call) Run(run func(rec storage.Record)) *Manager_Add_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 storage.Record if args[0] != nil { @@ -77,18 +77,18 @@ func (_c *Storage_Add_Call) Run(run func(rec storage.Record)) *Storage_Add_Call return _c } -func (_c *Storage_Add_Call) Return(err error) *Storage_Add_Call { +func (_c *Manager_Add_Call) Return(err error) *Manager_Add_Call { _c.Call.Return(err) return _c } -func (_c *Storage_Add_Call) RunAndReturn(run func(rec storage.Record) error) *Storage_Add_Call { +func (_c *Manager_Add_Call) RunAndReturn(run func(rec storage.Record) error) *Manager_Add_Call { _c.Call.Return(run) return _c } -// Get provides a mock function for the type Storage -func (_mock *Storage) Get(shareIDHash string) (storage.Record, error) { +// Get provides a mock function for the type Manager +func (_mock *Manager) Get(shareIDHash string) (storage.Record, error) { ret := _mock.Called(shareIDHash) if len(ret) == 0 { @@ -113,18 +113,18 @@ func (_mock *Storage) Get(shareIDHash string) (storage.Record, error) { return r0, r1 } -// Storage_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' -type Storage_Get_Call struct { +// Manager_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type Manager_Get_Call struct { *mock.Call } // Get is a helper method to define mock.On call // - shareIDHash string -func (_e *Storage_Expecter) Get(shareIDHash any) *Storage_Get_Call { - return &Storage_Get_Call{Call: _e.mock.On("Get", shareIDHash)} +func (_e *Manager_Expecter) Get(shareIDHash any) *Manager_Get_Call { + return &Manager_Get_Call{Call: _e.mock.On("Get", shareIDHash)} } -func (_c *Storage_Get_Call) Run(run func(shareIDHash string)) *Storage_Get_Call { +func (_c *Manager_Get_Call) Run(run func(shareIDHash string)) *Manager_Get_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { @@ -137,18 +137,18 @@ func (_c *Storage_Get_Call) Run(run func(shareIDHash string)) *Storage_Get_Call return _c } -func (_c *Storage_Get_Call) Return(record storage.Record, err error) *Storage_Get_Call { +func (_c *Manager_Get_Call) Return(record storage.Record, err error) *Manager_Get_Call { _c.Call.Return(record, err) return _c } -func (_c *Storage_Get_Call) RunAndReturn(run func(shareIDHash string) (storage.Record, error)) *Storage_Get_Call { +func (_c *Manager_Get_Call) RunAndReturn(run func(shareIDHash string) (storage.Record, error)) *Manager_Get_Call { _c.Call.Return(run) return _c } -// Redeem provides a mock function for the type Storage -func (_mock *Storage) Redeem(shareIDHash string) error { +// Redeem provides a mock function for the type Manager +func (_mock *Manager) Redeem(shareIDHash string) error { ret := _mock.Called(shareIDHash) if len(ret) == 0 { @@ -164,18 +164,18 @@ func (_mock *Storage) Redeem(shareIDHash string) error { return r0 } -// Storage_Redeem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Redeem' -type Storage_Redeem_Call struct { +// Manager_Redeem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Redeem' +type Manager_Redeem_Call struct { *mock.Call } // Redeem is a helper method to define mock.On call // - shareIDHash string -func (_e *Storage_Expecter) Redeem(shareIDHash any) *Storage_Redeem_Call { - return &Storage_Redeem_Call{Call: _e.mock.On("Redeem", shareIDHash)} +func (_e *Manager_Expecter) Redeem(shareIDHash any) *Manager_Redeem_Call { + return &Manager_Redeem_Call{Call: _e.mock.On("Redeem", shareIDHash)} } -func (_c *Storage_Redeem_Call) Run(run func(shareIDHash string)) *Storage_Redeem_Call { +func (_c *Manager_Redeem_Call) Run(run func(shareIDHash string)) *Manager_Redeem_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { @@ -188,18 +188,18 @@ func (_c *Storage_Redeem_Call) Run(run func(shareIDHash string)) *Storage_Redeem return _c } -func (_c *Storage_Redeem_Call) Return(err error) *Storage_Redeem_Call { +func (_c *Manager_Redeem_Call) Return(err error) *Manager_Redeem_Call { _c.Call.Return(err) return _c } -func (_c *Storage_Redeem_Call) RunAndReturn(run func(shareIDHash string) error) *Storage_Redeem_Call { +func (_c *Manager_Redeem_Call) RunAndReturn(run func(shareIDHash string) error) *Manager_Redeem_Call { _c.Call.Return(run) return _c } -// Remove provides a mock function for the type Storage -func (_mock *Storage) Remove(shareIDHash string) error { +// Remove provides a mock function for the type Manager +func (_mock *Manager) Remove(shareIDHash string) error { ret := _mock.Called(shareIDHash) if len(ret) == 0 { @@ -215,18 +215,18 @@ func (_mock *Storage) Remove(shareIDHash string) error { return r0 } -// Storage_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove' -type Storage_Remove_Call struct { +// Manager_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove' +type Manager_Remove_Call struct { *mock.Call } // Remove is a helper method to define mock.On call // - shareIDHash string -func (_e *Storage_Expecter) Remove(shareIDHash any) *Storage_Remove_Call { - return &Storage_Remove_Call{Call: _e.mock.On("Remove", shareIDHash)} +func (_e *Manager_Expecter) Remove(shareIDHash any) *Manager_Remove_Call { + return &Manager_Remove_Call{Call: _e.mock.On("Remove", shareIDHash)} } -func (_c *Storage_Remove_Call) Run(run func(shareIDHash string)) *Storage_Remove_Call { +func (_c *Manager_Remove_Call) Run(run func(shareIDHash string)) *Manager_Remove_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { @@ -239,12 +239,12 @@ func (_c *Storage_Remove_Call) Run(run func(shareIDHash string)) *Storage_Remove return _c } -func (_c *Storage_Remove_Call) Return(err error) *Storage_Remove_Call { +func (_c *Manager_Remove_Call) Return(err error) *Manager_Remove_Call { _c.Call.Return(err) return _c } -func (_c *Storage_Remove_Call) RunAndReturn(run func(shareIDHash string) error) *Storage_Remove_Call { +func (_c *Manager_Remove_Call) RunAndReturn(run func(shareIDHash string) error) *Manager_Remove_Call { _c.Call.Return(run) return _c } diff --git a/services/guestauth/pkg/service/storage/storage.go b/services/guestauth/pkg/service/storage/storage.go index a92ecf2dec..e19efd5ad0 100644 --- a/services/guestauth/pkg/service/storage/storage.go +++ b/services/guestauth/pkg/service/storage/storage.go @@ -17,7 +17,7 @@ type Record struct { Redeemed bool `json:"redeemed"` } -type Storage interface { +type Manager interface { Add(rec Record) error Get(shareIDHash string) (Record, error) Remove(shareIDHash string) error From e5c3be7c672a3b3ccf95625f3e9aeae06a02ebbf Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Tue, 29 Sep 2026 12:26:07 +0200 Subject: [PATCH 18/32] feat(guestauth): moved SecretHash to a method instead of field in struct --- services/guestauth/pkg/service/guestauth/service.go | 2 +- .../guestauth/pkg/service/guestauth/service_test.go | 4 ++-- .../guestauth/pkg/service/storage/file_manager_test.go | 2 +- services/guestauth/pkg/service/token/token.go | 10 +++++----- services/guestauth/pkg/service/token/token_test.go | 10 +++++----- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/services/guestauth/pkg/service/guestauth/service.go b/services/guestauth/pkg/service/guestauth/service.go index 7bd26a2f61..95ff8324b0 100644 --- a/services/guestauth/pkg/service/guestauth/service.go +++ b/services/guestauth/pkg/service/guestauth/service.go @@ -70,7 +70,7 @@ func (s *GuestAuthService) CreateToken(ctx context.Context, shareID string) (*to if err := s.store.Add(storage.Record{ ShareID: shareID, ShareIDHash: tok.ShareIDHash, - SecretHash: tok.SecretHash, + SecretHash: tok.SecretHash(), Expiry: utils.TSToTime(share.GetExpiration()), Redeemed: false, }); err != nil { diff --git a/services/guestauth/pkg/service/guestauth/service_test.go b/services/guestauth/pkg/service/guestauth/service_test.go index 9ec8868850..1b32275a14 100644 --- a/services/guestauth/pkg/service/guestauth/service_test.go +++ b/services/guestauth/pkg/service/guestauth/service_test.go @@ -54,7 +54,7 @@ func newToken(t *testing.T) (string, storage.Record) { rec := storage.Record{ ShareID: testShareID, ShareIDHash: tok.ShareIDHash, - SecretHash: tok.SecretHash, + SecretHash: tok.SecretHash(), Expiry: time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC), } @@ -111,7 +111,7 @@ func TestCreateTokenPersistsRecord(t *testing.T) { store.AssertCalled(t, "Add", mock.Anything) assert.Equal(t, testShareID, added.ShareID) assert.Equal(t, tok.ShareIDHash, added.ShareIDHash) - assert.Equal(t, tok.SecretHash, added.SecretHash) + assert.Equal(t, tok.SecretHash(), added.SecretHash) assert.True(t, expiry.Equal(added.Expiry)) assert.False(t, added.Redeemed) } diff --git a/services/guestauth/pkg/service/storage/file_manager_test.go b/services/guestauth/pkg/service/storage/file_manager_test.go index 3b2418d27d..fd98e821ac 100644 --- a/services/guestauth/pkg/service/storage/file_manager_test.go +++ b/services/guestauth/pkg/service/storage/file_manager_test.go @@ -16,7 +16,7 @@ func newRecord(shareID string) Record { return Record{ ShareID: shareID, ShareIDHash: tok.ShareIDHash, - SecretHash: tok.SecretHash, + SecretHash: tok.SecretHash(), Expiry: time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC), } } diff --git a/services/guestauth/pkg/service/token/token.go b/services/guestauth/pkg/service/token/token.go index 5c42431a51..c4c7214c9f 100644 --- a/services/guestauth/pkg/service/token/token.go +++ b/services/guestauth/pkg/service/token/token.go @@ -19,7 +19,6 @@ var ErrInvalidToken = errors.New("invalid token") type Token struct { ShareIDHash string - SecretHash string secret string } @@ -27,6 +26,10 @@ func (t *Token) String() string { return strings.Join([]string{tokenVersion, t.ShareIDHash, t.secret}, ".") } +func (t *Token) SecretHash() string { + return Hash(t.secret) +} + type TokenService struct{} func NewTokenService() *TokenService { @@ -42,7 +45,6 @@ func (s *TokenService) Generate(shareID string) (*Token, error) { secret := base64.RawURLEncoding.EncodeToString(secretBytes) return &Token{ ShareIDHash: Hash(shareID), - SecretHash: Hash(secret), secret: secret, }, nil } @@ -55,14 +57,12 @@ func (s *TokenService) Parse(encoded string) (*Token, error) { return &Token{ ShareIDHash: parts[1], - SecretHash: Hash(parts[2]), secret: parts[2], }, nil } func (s *TokenService) Verify(candidate Token, storedSecretHash string) error { - secretHash := Hash(candidate.secret) - if candidate.ShareIDHash == "" || candidate.secret == "" || candidate.SecretHash != secretHash || secretHash != storedSecretHash { + if candidate.ShareIDHash == "" || candidate.secret == "" || candidate.SecretHash() != storedSecretHash { return ErrInvalidToken } diff --git a/services/guestauth/pkg/service/token/token_test.go b/services/guestauth/pkg/service/token/token_test.go index 8f5c07f96b..1fc66285b3 100644 --- a/services/guestauth/pkg/service/token/token_test.go +++ b/services/guestauth/pkg/service/token/token_test.go @@ -15,7 +15,7 @@ func TestGenerateAndString(t *testing.T) { tok, err := svc.Generate(testShareID) require.NoError(t, err) assert.Equal(t, Hash(testShareID), tok.ShareIDHash) - assert.NotEmpty(t, tok.SecretHash) + assert.NotEmpty(t, tok.SecretHash()) assert.NotEmpty(t, tok.String()) } @@ -28,7 +28,7 @@ func TestGenerateRandomizesSecret(t *testing.T) { require.NoError(t, err) assert.Equal(t, tok1.ShareIDHash, tok2.ShareIDHash) - assert.NotEqual(t, tok1.SecretHash, tok2.SecretHash) + assert.NotEqual(t, tok1.SecretHash(), tok2.SecretHash()) assert.NotEqual(t, tok1.String(), tok2.String()) other, err := svc.Generate("9f9f9f9-9f9f-9f9f-9f9f-9f9f9f9f9f9f") @@ -64,7 +64,7 @@ func TestParse(t *testing.T) { require.NoError(t, err) assert.Equal(t, original.ShareIDHash, parsed.ShareIDHash) - assert.Equal(t, original.SecretHash, parsed.SecretHash) + assert.Equal(t, original.SecretHash(), parsed.SecretHash()) assert.Equal(t, original.String(), parsed.String()) }) } @@ -81,9 +81,9 @@ func TestVerify(t *testing.T) { storedSecretHash string wantErr bool }{ - {name: "valid", token: *tok, storedSecretHash: tok.SecretHash}, + {name: "valid", token: *tok, storedSecretHash: tok.SecretHash()}, {name: "wrong stored secret", token: *tok, storedSecretHash: Hash("other-secret"), wantErr: true}, - {name: "missing fields", token: Token{ShareIDHash: tok.ShareIDHash}, storedSecretHash: tok.SecretHash, wantErr: true}, + {name: "missing fields", token: Token{ShareIDHash: tok.ShareIDHash}, storedSecretHash: tok.SecretHash(), wantErr: true}, } for _, tt := range tests { From b92065481545219d80071f64b7fecda1525ea8b4 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Tue, 29 Sep 2026 12:33:17 +0200 Subject: [PATCH 19/32] feat(guetauth): upd the redeem route --- services/guestauth/README.md | 2 +- services/guestauth/pkg/server/http/server.go | 2 +- services/proxy/pkg/config/defaults/defaultconfig.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/guestauth/README.md b/services/guestauth/README.md index 8360c48ae1..196b9b541a 100644 --- a/services/guestauth/README.md +++ b/services/guestauth/README.md @@ -27,7 +27,7 @@ It is part of the default service set and does not need to be enabled with the hash of the share id. It then publishes the `GuestTokenCreated` event with the token. 2. **Redeem** — the guest posts the token to - `POST /graph/v1beta1/guestInvitations/redeem`. The service validates the + `POST /graph/v1beta1/extensions/org.libregraph/guestInvitations/redeem`. The service validates the token and the share, marks the token as used and returns a signed JWT session token in a cookie. Tokens are single-use. 3. **Cleanup** — on the consumed `ShareRemoved` or `ShareExpired` event, the diff --git a/services/guestauth/pkg/server/http/server.go b/services/guestauth/pkg/server/http/server.go index 468e6ee2fb..8f7d169d93 100644 --- a/services/guestauth/pkg/server/http/server.go +++ b/services/guestauth/pkg/server/http/server.go @@ -56,7 +56,7 @@ func Server(opts ...Option) (ohttp.Service, error) { mux.Use(middlewares...) mux.Route(options.Config.HTTP.Root, func(r chi.Router) { - r.Post("/v1beta1/guestInvitations/redeem", RedeemHandler(options.Logger, options.Service, options.Config)) + r.Post("/v1beta1/extensions/org.libregraph/guestInvitations/redeem", RedeemHandler(options.Logger, options.Service, options.Config)) }) err = micro.RegisterHandler(newService.Server(), mux) diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index 0db7029fb1..51ca7b373c 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -284,7 +284,7 @@ func DefaultPolicies() []config.Policy { Service: "eu.opencloud.web.invitations", }, { - Endpoint: "/graph/v1beta1/guestInvitations", + Endpoint: "/graph/v1beta1/extensions/org.libregraph/guestInvitations", Service: "eu.opencloud.web.guestauth", Unprotected: true, }, From 90882737786a2f38c7a730adbb0e576199eb8b3e Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Tue, 29 Sep 2026 12:40:34 +0200 Subject: [PATCH 20/32] feat(guestauth): upd token record expiry proporety value on add --- services/guestauth/pkg/service/guestauth/service.go | 9 +++------ services/guestauth/pkg/service/guestauth/service_test.go | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/services/guestauth/pkg/service/guestauth/service.go b/services/guestauth/pkg/service/guestauth/service.go index 95ff8324b0..cb573830dc 100644 --- a/services/guestauth/pkg/service/guestauth/service.go +++ b/services/guestauth/pkg/service/guestauth/service.go @@ -23,6 +23,8 @@ var ErrAlreadyRedeemed = errors.New("token already redeemed") var ErrShareNotFound = errors.New("share not found") var ErrShareExpired = errors.New("share expired") +const invitationTokenTTL = 30 * time.Minute + // GuestAuth is the domain service used by the transport and event layers. type GuestAuth interface { CreateToken(ctx context.Context, shareID string) (*token.Token, error) @@ -62,16 +64,11 @@ func (s *GuestAuthService) CreateToken(ctx context.Context, shareID string) (*to return nil, err } - share, err := s.getShare(ctx, shareID) - if err != nil { - return nil, err - } - if err := s.store.Add(storage.Record{ ShareID: shareID, ShareIDHash: tok.ShareIDHash, SecretHash: tok.SecretHash(), - Expiry: utils.TSToTime(share.GetExpiration()), + Expiry: time.Now().Add(invitationTokenTTL), Redeemed: false, }); err != nil { return nil, err diff --git a/services/guestauth/pkg/service/guestauth/service_test.go b/services/guestauth/pkg/service/guestauth/service_test.go index 1b32275a14..3bb63f83c0 100644 --- a/services/guestauth/pkg/service/guestauth/service_test.go +++ b/services/guestauth/pkg/service/guestauth/service_test.go @@ -112,7 +112,7 @@ func TestCreateTokenPersistsRecord(t *testing.T) { assert.Equal(t, testShareID, added.ShareID) assert.Equal(t, tok.ShareIDHash, added.ShareIDHash) assert.Equal(t, tok.SecretHash(), added.SecretHash) - assert.True(t, expiry.Equal(added.Expiry)) + assert.WithinDuration(t, time.Now().Add(invitationTokenTTL), added.Expiry, time.Minute) assert.False(t, added.Redeemed) } From eac68b9354ff1b132d7c5a83c541e01b13c26be6 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Tue, 29 Sep 2026 13:52:51 +0200 Subject: [PATCH 21/32] feat(guestauth): upd redeem response error format --- services/guestauth/pkg/server/http/errors.go | 55 +++++++++++++++++++ services/guestauth/pkg/server/http/redeem.go | 31 +---------- .../guestauth/pkg/server/http/redeem_test.go | 54 ++++++++++++++++-- .../pkg/service/guestauth/service.go | 29 ++++++---- .../pkg/service/guestauth/service_test.go | 14 +++-- 5 files changed, 135 insertions(+), 48 deletions(-) create mode 100644 services/guestauth/pkg/server/http/errors.go diff --git a/services/guestauth/pkg/server/http/errors.go b/services/guestauth/pkg/server/http/errors.go new file mode 100644 index 0000000000..3fdf890b00 --- /dev/null +++ b/services/guestauth/pkg/server/http/errors.go @@ -0,0 +1,55 @@ +package http + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" + "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" +) + +type errorResponse struct { + ErrorType string `json:"error_type"` + Message string `json:"message"` + ShareID string `json:"share_id"` +} + +func writeError(w http.ResponseWriter, status int, body errorResponse) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(body) +} + +func writeRedeemError(w http.ResponseWriter, err error) { + var re *guestauth.RedeemError + if !errors.As(err, &re) { + writeError(w, http.StatusInternalServerError, errorResponse{ErrorType: "internal_error", Message: "An internal error occurred."}) + return + } + + status := http.StatusInternalServerError + errorType := "internal_error" + switch { + case errors.Is(re.ErrorType, guestauth.ErrExpired): + status, errorType = http.StatusUnauthorized, "token_expired" + case errors.Is(re.ErrorType, token.ErrInvalidToken): + status, errorType = http.StatusUnauthorized, "token_invalid" + case errors.Is(re.ErrorType, storage.ErrNotFound): + status, errorType = http.StatusNotFound, "token_not_found" + case errors.Is(re.ErrorType, guestauth.ErrAlreadyRedeemed): + status, errorType = http.StatusConflict, "token_already_redeemed" + case errors.Is(re.ErrorType, guestauth.ErrShareNotFound): + status, errorType = http.StatusNotFound, "share_not_found" + case errors.Is(re.ErrorType, guestauth.ErrShareExpired): + status, errorType = http.StatusGone, "share_expired" + } + + message := re.ErrorType.Error() + if errorType == "internal_error" { + message = "An internal error occurred." + } + + writeError(w, status, errorResponse{ErrorType: errorType, Message: message, ShareID: re.ShareID}) +} diff --git a/services/guestauth/pkg/server/http/redeem.go b/services/guestauth/pkg/server/http/redeem.go index ce88d78530..78831017ea 100644 --- a/services/guestauth/pkg/server/http/redeem.go +++ b/services/guestauth/pkg/server/http/redeem.go @@ -2,14 +2,11 @@ package http import ( "encoding/json" - "errors" "net/http" "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" - token "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" ) // RedeemRequest is the request body for token redemption. @@ -23,36 +20,14 @@ func RedeemHandler(log log.Logger, s guestauth.GuestAuth, cfg *config.Config) fu var req RedeemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { log.Debug().Err(err).Msg("request body is malformed") - w.WriteHeader(http.StatusBadRequest) + writeError(w, http.StatusBadRequest, errorResponse{ErrorType: "invalid_request", Message: "The request body is malformed."}) return } sessionToken, err := s.Redeem(r.Context(), req.Token) if err != nil { - switch { - case errors.Is(err, guestauth.ErrAlreadyRedeemed): - log.Debug().Err(err).Msg("token already redeemed") - w.WriteHeader(http.StatusConflict) - case errors.Is(err, guestauth.ErrExpired): - log.Debug().Err(err).Msg("token expired") - w.WriteHeader(http.StatusGone) - case errors.Is(err, storage.ErrNotFound): - log.Debug().Err(err).Msg("token not found") - w.WriteHeader(http.StatusNotFound) - case errors.Is(err, token.ErrInvalidToken): - log.Debug().Err(err).Msg("token is invalid") - w.WriteHeader(http.StatusUnauthorized) - case errors.Is(err, guestauth.ErrShareNotFound): - log.Debug().Err(err).Msg("share not found") - w.WriteHeader(http.StatusNotFound) - case errors.Is(err, guestauth.ErrShareExpired): - log.Debug().Err(err).Msg("share expired") - w.WriteHeader(http.StatusGone) - - default: - log.Error().Err(err).Msg("error redeeming token") - w.WriteHeader(http.StatusInternalServerError) - } + log.Debug().Err(err).Msg("redeem failed") + writeRedeemError(w, err) return } diff --git a/services/guestauth/pkg/server/http/redeem_test.go b/services/guestauth/pkg/server/http/redeem_test.go index d3fda98281..cdae0c5eb2 100644 --- a/services/guestauth/pkg/server/http/redeem_test.go +++ b/services/guestauth/pkg/server/http/redeem_test.go @@ -59,13 +59,46 @@ func TestRedeemHandlerErrorMapping(t *testing.T) { name string err error wantStatus int + wantType string + wantShare string }{ - {name: "already redeemed", err: guestauth.ErrAlreadyRedeemed, wantStatus: http.StatusConflict}, - {name: "token expired", err: guestauth.ErrExpired, wantStatus: http.StatusGone}, - {name: "token not found", err: storage.ErrNotFound, wantStatus: http.StatusNotFound}, - {name: "invalid token", err: token.ErrInvalidToken, wantStatus: http.StatusUnauthorized}, - {name: "share not found", err: guestauth.ErrShareNotFound, wantStatus: http.StatusNotFound}, - {name: "share expired", err: guestauth.ErrShareExpired, wantStatus: http.StatusGone}, + { + name: "token expired", + err: &guestauth.RedeemError{ErrorType: guestauth.ErrExpired, ShareID: "share-1"}, + wantStatus: http.StatusUnauthorized, + wantType: "token_expired", + wantShare: "share-1", + }, + { + name: "token invalid", + err: &guestauth.RedeemError{ErrorType: token.ErrInvalidToken}, + wantStatus: http.StatusUnauthorized, + wantType: "token_invalid", + }, + { + name: "token not found", + err: &guestauth.RedeemError{ErrorType: storage.ErrNotFound}, + wantStatus: http.StatusNotFound, + wantType: "token_not_found", + }, + { + name: "token already redeemed", + err: &guestauth.RedeemError{ErrorType: guestauth.ErrAlreadyRedeemed}, + wantStatus: http.StatusConflict, + wantType: "token_already_redeemed", + }, + { + name: "share not found", + err: &guestauth.RedeemError{ErrorType: guestauth.ErrShareNotFound}, + wantStatus: http.StatusNotFound, + wantType: "share_not_found", + }, + { + name: "share expired", + err: &guestauth.RedeemError{ErrorType: guestauth.ErrShareExpired}, + wantStatus: http.StatusGone, + wantType: "share_expired", + }, } for _, tt := range tests { @@ -80,6 +113,11 @@ func TestRedeemHandlerErrorMapping(t *testing.T) { newRedeemHandler(t, svcMock)(rr, httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(body)))) assert.Equal(t, tt.wantStatus, rr.Code) + + var resp errorResponse + require.NoError(t, json.NewDecoder(rr.Body).Decode(&resp)) + assert.Equal(t, tt.wantType, resp.ErrorType) + assert.Equal(t, tt.wantShare, resp.ShareID) }) } } @@ -91,4 +129,8 @@ func TestRedeemHandlerMalformedBody(t *testing.T) { newRedeemHandler(t, svcMock)(rr, httptest.NewRequest(http.MethodPost, "/", strings.NewReader("not-json"))) assert.Equal(t, http.StatusBadRequest, rr.Code) + + var resp errorResponse + require.NoError(t, json.NewDecoder(rr.Body).Decode(&resp)) + assert.Equal(t, "invalid_request", resp.ErrorType) } diff --git a/services/guestauth/pkg/service/guestauth/service.go b/services/guestauth/pkg/service/guestauth/service.go index cb573830dc..710b7cca49 100644 --- a/services/guestauth/pkg/service/guestauth/service.go +++ b/services/guestauth/pkg/service/guestauth/service.go @@ -25,6 +25,15 @@ var ErrShareExpired = errors.New("share expired") const invitationTokenTTL = 30 * time.Minute +// RedeemError wraps a redeem failure together with the share id. The HTTP +// transport inspects ErrorType to choose a status code and message. +type RedeemError struct { + ErrorType error + ShareID string +} + +func (e *RedeemError) Error() string { return e.ErrorType.Error() } + // GuestAuth is the domain service used by the transport and event layers. type GuestAuth interface { CreateToken(ctx context.Context, shareID string) (*token.Token, error) @@ -90,7 +99,7 @@ func (s *GuestAuthService) Redeem(ctx context.Context, tokenString string) (stri if err := s.store.Redeem(rec.ShareIDHash); err != nil { if errors.Is(err, storage.ErrAlreadyRedeemed) { - return "", ErrAlreadyRedeemed + return "", &RedeemError{ErrorType: ErrAlreadyRedeemed, ShareID: rec.ShareID} } return "", err } @@ -110,41 +119,41 @@ func (s *GuestAuthService) CleanupShare(shareID string) error { } // VerifyToken validates a token and returns its stored record. -func (s *GuestAuthService) verifyToken(tokenString string) (storage.Record, error) { +func (s *GuestAuthService) verifyToken(tokenString string) (*storage.Record, error) { tok, err := s.tokenSvc.Parse(tokenString) if err != nil { - return storage.Record{}, err + return nil, &RedeemError{ErrorType: err} } rec, err := s.store.Get(tok.ShareIDHash) if err != nil { - return storage.Record{}, err + return nil, &RedeemError{ErrorType: err} } if err := s.tokenSvc.Verify(*tok, rec.SecretHash); err != nil { - return storage.Record{}, err + return nil, &RedeemError{ErrorType: err, ShareID: rec.ShareID} } if !rec.Expiry.IsZero() && rec.Expiry.Before(time.Now()) { - return storage.Record{}, ErrExpired + return nil, &RedeemError{ErrorType: ErrExpired, ShareID: rec.ShareID} } if rec.Redeemed { - return storage.Record{}, ErrAlreadyRedeemed + return nil, &RedeemError{ErrorType: ErrAlreadyRedeemed, ShareID: rec.ShareID} } - return rec, nil + return &rec, nil } // validateShare extracts the share information from the gateway and checks its existence and expiration. func (s *GuestAuthService) validateShare(ctx context.Context, shareID string) (*collaboration.Share, error) { share, err := s.getShare(ctx, shareID) if err != nil { - return nil, err + return nil, &RedeemError{ErrorType: err, ShareID: shareID} } if exp := utils.TSToTime(share.GetExpiration()); !exp.IsZero() && exp.Before(time.Now()) { - return nil, ErrShareExpired + return nil, &RedeemError{ErrorType: ErrShareExpired, ShareID: shareID} } return share, nil diff --git a/services/guestauth/pkg/service/guestauth/service_test.go b/services/guestauth/pkg/service/guestauth/service_test.go index 3bb63f83c0..38253a3f52 100644 --- a/services/guestauth/pkg/service/guestauth/service_test.go +++ b/services/guestauth/pkg/service/guestauth/service_test.go @@ -143,12 +143,14 @@ func TestVerifyToken(t *testing.T) { got, err := s.verifyToken(tok) if tt.wantErr != nil { - assert.ErrorIs(t, err, tt.wantErr) + var re *RedeemError + require.ErrorAs(t, err, &re) + assert.ErrorIs(t, re.ErrorType, tt.wantErr) return } require.NoError(t, err) - assert.Equal(t, rec, got) + assert.Equal(t, rec, *got) }) } } @@ -200,7 +202,9 @@ func TestValidateShare(t *testing.T) { got, err := s.validateShare(context.Background(), testShareID) if tt.wantErr != nil { - assert.ErrorIs(t, err, tt.wantErr) + var re *RedeemError + require.ErrorAs(t, err, &re) + assert.ErrorIs(t, re.ErrorType, tt.wantErr) return } @@ -242,5 +246,7 @@ func TestRedeemAlreadyRedeemed(t *testing.T) { })) _, err := s.Redeem(context.Background(), tok) - assert.ErrorIs(t, err, ErrAlreadyRedeemed) + var re *RedeemError + require.ErrorAs(t, err, &re) + assert.ErrorIs(t, re.ErrorType, ErrAlreadyRedeemed) } From cf77e47ce470095aa11611d395e18de28dc05fbc Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Tue, 29 Sep 2026 14:50:15 +0200 Subject: [PATCH 22/32] feat(guestauth): used file lock instead of mutex in file strorage implementation --- services/guestauth/pkg/server/http/errors.go | 2 + .../pkg/service/storage/file_manager.go | 89 +++++++++++++++---- .../pkg/service/storage/file_manager_test.go | 74 +++++++++++++-- .../guestauth/pkg/service/storage/storage.go | 1 + 4 files changed, 144 insertions(+), 22 deletions(-) diff --git a/services/guestauth/pkg/server/http/errors.go b/services/guestauth/pkg/server/http/errors.go index 3fdf890b00..14b542e24c 100644 --- a/services/guestauth/pkg/server/http/errors.go +++ b/services/guestauth/pkg/server/http/errors.go @@ -38,6 +38,8 @@ func writeRedeemError(w http.ResponseWriter, err error) { status, errorType = http.StatusUnauthorized, "token_invalid" case errors.Is(re.ErrorType, storage.ErrNotFound): status, errorType = http.StatusNotFound, "token_not_found" + case errors.Is(re.ErrorType, storage.ErrInvalidHash): + status, errorType = http.StatusUnauthorized, "token_invalid" case errors.Is(re.ErrorType, guestauth.ErrAlreadyRedeemed): status, errorType = http.StatusConflict, "token_already_redeemed" case errors.Is(re.ErrorType, guestauth.ErrShareNotFound): diff --git a/services/guestauth/pkg/service/storage/file_manager.go b/services/guestauth/pkg/service/storage/file_manager.go index 2324036302..241a30ad62 100644 --- a/services/guestauth/pkg/service/storage/file_manager.go +++ b/services/guestauth/pkg/service/storage/file_manager.go @@ -7,8 +7,9 @@ import ( "io/fs" "os" "path/filepath" - "sync" + "strings" + "github.com/gofrs/flock" "github.com/google/renameio/v2" ) @@ -20,15 +21,31 @@ func NewFileManager(root string) *FileManager { type FileManager struct { root string - mu sync.Mutex } const dirPerm = 0700 const filePerm = 0600 +// minHashLength is the minimum share id hash length needed to derive a path. +const minHashLength = 4 + func (s *FileManager) Add(rec Record) error { - s.mu.Lock() - defer s.mu.Unlock() + lock, err := s.lockStore() + if err != nil { + return err + } + defer func() { _ = lock.Unlock() }() + + p, err := s.path(rec.ShareIDHash) + if err != nil { + return err + } + + if _, err := os.Stat(p); err == nil { + return fmt.Errorf("record %q already exists: %w", rec.ShareIDHash, fs.ErrExist) + } else if !errors.Is(err, fs.ErrNotExist) { + return err + } return s.add(rec) } @@ -39,18 +56,21 @@ func (s *FileManager) Get(shareIDHash string) (Record, error) { } func (s *FileManager) Remove(shareIDHash string) error { - s.mu.Lock() - defer s.mu.Unlock() + lock, err := s.lockStore() + if err != nil { + return err + } + defer func() { _ = lock.Unlock() }() - p := s.path(shareIDHash) - if _, err := os.Stat(p); err != nil { - if errors.Is(err, fs.ErrNotExist) { - return ErrNotFound - } + p, err := s.path(shareIDHash) + if err != nil { return err } if err := os.Remove(p); err != nil { + if errors.Is(err, fs.ErrNotExist) { + return ErrNotFound + } return err } @@ -58,8 +78,11 @@ func (s *FileManager) Remove(shareIDHash string) error { } func (s *FileManager) Redeem(shareIDHash string) error { - s.mu.Lock() - defer s.mu.Unlock() + lock, err := s.lockStore() + if err != nil { + return err + } + defer func() { _ = lock.Unlock() }() rec, err := s.get(shareIDHash) if err != nil { @@ -74,13 +97,30 @@ func (s *FileManager) Redeem(shareIDHash string) error { return s.add(rec) } +func (s *FileManager) lockStore() (*flock.Flock, error) { + if err := os.MkdirAll(s.root, dirPerm); err != nil { + return nil, fmt.Errorf("could not create directory %s: %w", s.root, err) + } + + lock := flock.New(filepath.Join(s.root, ".lock")) + if err := lock.Lock(); err != nil { + return nil, err + } + + return lock, nil +} + func (s *FileManager) add(rec Record) error { + p, err := s.path(rec.ShareIDHash) + if err != nil { + return err + } + data, err := json.Marshal(rec) if err != nil { return err } - p := s.path(rec.ShareIDHash) dir := filepath.Dir(p) if err := os.MkdirAll(dir, dirPerm); err != nil { return fmt.Errorf("could not create directory %s: %w", dir, err) @@ -90,7 +130,12 @@ func (s *FileManager) add(rec Record) error { } func (s *FileManager) get(shareIDHash string) (Record, error) { - data, err := os.ReadFile(s.path(shareIDHash)) + p, err := s.path(shareIDHash) + if err != nil { + return Record{}, err + } + + data, err := os.ReadFile(p) if err != nil { if errors.Is(err, fs.ErrNotExist) { return Record{}, ErrNotFound @@ -106,6 +151,16 @@ func (s *FileManager) get(shareIDHash string) (Record, error) { return rec, nil } -func (s *FileManager) path(shareIDHash string) string { - return filepath.Join(s.root, shareIDHash[:2], shareIDHash[2:4], shareIDHash[4:]+".json") +func (s *FileManager) path(shareIDHash string) (string, error) { + if len(shareIDHash) < minHashLength { + return "", ErrInvalidHash + } + + p := filepath.Join(s.root, shareIDHash[:2], shareIDHash[2:4], shareIDHash[4:]+".json") + root := filepath.Clean(s.root) + if !strings.HasPrefix(p, root+string(os.PathSeparator)) { + return "", ErrInvalidHash + } + + return p, nil } diff --git a/services/guestauth/pkg/service/storage/file_manager_test.go b/services/guestauth/pkg/service/storage/file_manager_test.go index fd98e821ac..ebf2bc3df8 100644 --- a/services/guestauth/pkg/service/storage/file_manager_test.go +++ b/services/guestauth/pkg/service/storage/file_manager_test.go @@ -1,6 +1,9 @@ package storage import ( + "io/fs" + "sync" + "sync/atomic" "testing" "time" @@ -41,7 +44,7 @@ func TestFileManagerGetMissing(t *testing.T) { assert.ErrorIs(t, err, ErrNotFound) } -func TestFileManagerAddOverwrites(t *testing.T) { +func TestFileManagerAddExisting(t *testing.T) { dir := t.TempDir() s := NewFileManager(dir) @@ -49,11 +52,21 @@ func TestFileManagerAddOverwrites(t *testing.T) { require.NoError(t, s.Add(rec)) rec.SecretHash = "other" - require.NoError(t, s.Add(rec)) + require.ErrorIs(t, s.Add(rec), fs.ErrExist) +} - got, err := s.Get(rec.ShareIDHash) - require.NoError(t, err) - assert.Equal(t, "other", got.SecretHash) +func TestFileManagerInvalidHash(t *testing.T) { + dir := t.TempDir() + s := NewFileManager(dir) + + _, err := s.Get("ab") + require.ErrorIs(t, err, ErrInvalidHash) + + _, err = s.Get("../../etc/passwd-xyz") + require.ErrorIs(t, err, ErrInvalidHash) + + require.ErrorIs(t, s.Remove("ab"), ErrInvalidHash) + require.ErrorIs(t, s.Add(Record{ShareIDHash: "ab"}), ErrInvalidHash) } func TestFileManagerRemove(t *testing.T) { @@ -101,3 +114,54 @@ func TestFileManagerRedeemMissing(t *testing.T) { err := s.Redeem("doesnotexist") assert.ErrorIs(t, err, ErrNotFound) } + +func TestFileManagerAddConcurrent(t *testing.T) { + dir := t.TempDir() + s := NewFileManager(dir) + + rec := newRecord("e0123456-7890-abcd-ef01-234567890abc") + + const workers = 20 + var ( + wg sync.WaitGroup + success atomic.Int32 + ) + for range workers { + wg.Add(1) + go func() { + defer wg.Done() + if err := s.Add(rec); err == nil { + success.Add(1) + } + }() + } + wg.Wait() + + assert.Equal(t, int32(1), success.Load()) +} + +func TestFileManagerRedeemConcurrent(t *testing.T) { + dir := t.TempDir() + s := NewFileManager(dir) + + rec := newRecord("e0123456-7890-abcd-ef01-234567890abc") + require.NoError(t, s.Add(rec)) + + const workers = 20 + var ( + wg sync.WaitGroup + success atomic.Int32 + ) + for range workers { + wg.Add(1) + go func() { + defer wg.Done() + if err := s.Redeem(rec.ShareIDHash); err == nil { + success.Add(1) + } + }() + } + wg.Wait() + + assert.Equal(t, int32(1), success.Load()) +} diff --git a/services/guestauth/pkg/service/storage/storage.go b/services/guestauth/pkg/service/storage/storage.go index e19efd5ad0..eec6ab6cca 100644 --- a/services/guestauth/pkg/service/storage/storage.go +++ b/services/guestauth/pkg/service/storage/storage.go @@ -7,6 +7,7 @@ import ( var ErrNotFound = errors.New("record not found") var ErrAlreadyRedeemed = errors.New("token already redeemed") +var ErrInvalidHash = errors.New("invalid share id hash") // Record holds the data persisted for a guest share token. type Record struct { From f86e339233511a959c27935455c2c847dd3c7a1a Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Tue, 29 Sep 2026 15:21:57 +0200 Subject: [PATCH 23/32] feat(guestauth): grantee email in GuestTokenCreated event --- pkg/events/events.go | 1 + services/guestauth/pkg/service/events/handlers.go | 1 + services/guestauth/pkg/service/events/handlers_test.go | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/events/events.go b/pkg/events/events.go index 79124ff233..4dfb695b61 100644 --- a/pkg/events/events.go +++ b/pkg/events/events.go @@ -25,6 +25,7 @@ func (ResourceMention) Unmarshal(v []byte) (interface{}, error) { type GuestTokenCreated struct { ShareID *collaboration.ShareId Sharer *user.UserId + GranteeEmail string ItemID *provider.ResourceId ResourceName string Token string diff --git a/services/guestauth/pkg/service/events/handlers.go b/services/guestauth/pkg/service/events/handlers.go index c86a4e22b2..ca41413da8 100644 --- a/services/guestauth/pkg/service/events/handlers.go +++ b/services/guestauth/pkg/service/events/handlers.go @@ -27,6 +27,7 @@ func (s *EventConsumer) handleShareCreated(ctx context.Context, ev events.ShareC return events.Publish(ctx, s.stream, ocEvents.GuestTokenCreated{ ShareID: ev.ShareID, Sharer: ev.Sharer, + GranteeEmail: ev.GranteeUserID.GetOpaqueId(), ItemID: ev.ItemID, ResourceName: ev.ResourceName, Token: tok.String(), diff --git a/services/guestauth/pkg/service/events/handlers_test.go b/services/guestauth/pkg/service/events/handlers_test.go index c19f516816..cfea2ede84 100644 --- a/services/guestauth/pkg/service/events/handlers_test.go +++ b/services/guestauth/pkg/service/events/handlers_test.go @@ -56,7 +56,7 @@ func TestHandleShareCreated(t *testing.T) { Sharer: &user.UserId{OpaqueId: "sharer"}, ItemID: &provider.ResourceId{StorageId: "storage", OpaqueId: "item"}, ResourceName: "resource", - GranteeUserID: &user.UserId{OpaqueId: "guest", Type: user.UserType_USER_TYPE_GUEST}, + GranteeUserID: &user.UserId{OpaqueId: "guest@example.org", Type: user.UserType_USER_TYPE_GUEST}, } require.NoError(t, svc.handleShareCreated(context.Background(), ev)) @@ -68,6 +68,7 @@ func TestHandleShareCreated(t *testing.T) { require.True(t, ok) assert.Equal(t, testShareID, published.ShareID.GetOpaqueId()) assert.Equal(t, ev.Sharer, published.Sharer) + assert.Equal(t, "guest@example.org", published.GranteeEmail) assert.Equal(t, ev.ItemID, published.ItemID) assert.Equal(t, ev.ResourceName, published.ResourceName) assert.Equal(t, tok.String(), published.Token) From cbf3ee8c0b34021fc58f3fdd3b88141170958d63 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Tue, 29 Sep 2026 15:58:20 +0200 Subject: [PATCH 24/32] feat(guestauth): guestauth service rename to auth-guest --- Makefile | 2 +- opencloud/pkg/command/services.go | 6 +- opencloud/pkg/init/init.go | 18 ++-- opencloud/pkg/init/structs.go | 6 +- opencloud/pkg/runtime/service/service.go | 10 +-- pkg/config/config.go | 4 +- pkg/config/defaultconfig.go | 4 +- .../{guestauth => auth-guest}/.mockery.yaml | 6 +- services/{guestauth => auth-guest}/Makefile | 2 +- services/{guestauth => auth-guest}/README.md | 22 ++--- .../pkg/command/health.go | 5 +- .../pkg/command/root.go | 11 ++- .../pkg/command/server.go | 35 ++++---- .../pkg/command/version.go | 5 +- services/auth-guest/pkg/config/config.go | 90 +++++++++++++++++++ services/auth-guest/pkg/config/debug.go | 12 +++ .../pkg/config/defaults/defaultconfig.go | 9 +- .../pkg/config/parser/parse.go | 7 +- .../pkg/config/service.go | 3 + .../pkg/metrics/metrics.go | 5 +- .../pkg/server/debug/option.go | 5 +- .../pkg/server/debug/server.go | 3 + .../pkg/server/http/errors.go | 19 ++-- .../pkg/server/http/option.go | 11 ++- .../pkg/server/http/redeem.go | 9 +- .../pkg/server/http/redeem_test.go | 33 +++---- .../pkg/server/http/server.go | 3 + .../service/authguest/mocks/auth_guest.go} | 74 +++++++-------- .../pkg/service/authguest}/options.go | 15 ++-- .../pkg/service/authguest}/service.go | 39 ++++---- .../pkg/service/authguest}/service_test.go | 27 +++--- .../pkg/service/events/handlers.go | 9 +- .../pkg/service/events/handlers_test.go | 21 +++-- .../pkg/service/events/options.go | 23 ++--- .../pkg/service/events/service.go | 15 ++-- .../pkg/service/jwt/jwt.go | 3 + .../pkg/service/jwt/jwt_test.go | 3 + .../pkg/service/storage/file_manager.go | 3 + .../pkg/service/storage/file_manager_test.go | 5 +- .../pkg/service/storage/mocks/manager.go | 2 +- .../pkg/service/storage/storage.go | 3 + .../pkg/service/token/token.go | 3 + .../pkg/service/token/token_test.go | 3 + services/guestauth/pkg/config/config.go | 87 ------------------ services/guestauth/pkg/config/debug.go | 9 -- .../pkg/config/defaults/defaultconfig.go | 2 +- 46 files changed, 392 insertions(+), 299 deletions(-) rename services/{guestauth => auth-guest}/.mockery.yaml (57%) rename services/{guestauth => auth-guest}/Makefile (94%) rename services/{guestauth => auth-guest}/README.md (69%) rename services/{guestauth => auth-guest}/pkg/command/health.go (67%) rename services/{guestauth => auth-guest}/pkg/command/root.go (67%) rename services/{guestauth => auth-guest}/pkg/command/server.go (80%) rename services/{guestauth => auth-guest}/pkg/command/version.go (71%) create mode 100644 services/auth-guest/pkg/config/config.go create mode 100644 services/auth-guest/pkg/config/debug.go rename services/{guestauth => auth-guest}/pkg/config/defaults/defaultconfig.go (88%) rename services/{guestauth => auth-guest}/pkg/config/parser/parse.go (80%) rename services/{guestauth => auth-guest}/pkg/config/service.go (56%) rename services/{guestauth => auth-guest}/pkg/metrics/metrics.go (85%) rename services/{guestauth => auth-guest}/pkg/server/debug/option.go (85%) rename services/{guestauth => auth-guest}/pkg/server/debug/server.go (93%) rename services/{guestauth => auth-guest}/pkg/server/http/errors.go (72%) rename services/{guestauth => auth-guest}/pkg/server/http/option.go (79%) rename services/{guestauth => auth-guest}/pkg/server/http/redeem.go (78%) rename services/{guestauth => auth-guest}/pkg/server/http/redeem_test.go (74%) rename services/{guestauth => auth-guest}/pkg/server/http/server.go (95%) rename services/{guestauth/pkg/service/guestauth/mocks/guest_auth.go => auth-guest/pkg/service/authguest/mocks/auth_guest.go} (57%) rename services/{guestauth/pkg/service/guestauth => auth-guest/pkg/service/authguest}/options.go (63%) rename services/{guestauth/pkg/service/guestauth => auth-guest/pkg/service/authguest}/service.go (79%) rename services/{guestauth/pkg/service/guestauth => auth-guest/pkg/service/authguest}/service_test.go (90%) rename services/{guestauth => auth-guest}/pkg/service/events/handlers.go (84%) rename services/{guestauth => auth-guest}/pkg/service/events/handlers_test.go (84%) rename services/{guestauth => auth-guest}/pkg/service/events/options.go (61%) rename services/{guestauth => auth-guest}/pkg/service/events/service.go (87%) rename services/{guestauth => auth-guest}/pkg/service/jwt/jwt.go (88%) rename services/{guestauth => auth-guest}/pkg/service/jwt/jwt_test.go (91%) rename services/{guestauth => auth-guest}/pkg/service/storage/file_manager.go (97%) rename services/{guestauth => auth-guest}/pkg/service/storage/file_manager_test.go (95%) rename services/{guestauth => auth-guest}/pkg/service/storage/mocks/manager.go (98%) rename services/{guestauth => auth-guest}/pkg/service/storage/storage.go (87%) rename services/{guestauth => auth-guest}/pkg/service/token/token.go (94%) rename services/{guestauth => auth-guest}/pkg/service/token/token_test.go (96%) delete mode 100644 services/guestauth/pkg/config/config.go delete mode 100644 services/guestauth/pkg/config/debug.go diff --git a/Makefile b/Makefile index 23f2a9275d..dc672e093e 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,7 @@ OC_MODULES = \ services/auth-app \ services/auth-basic \ services/auth-bearer \ + services/auth-guest \ services/auth-machine \ services/auth-service \ services/clientlog \ @@ -39,7 +40,6 @@ OC_MODULES = \ services/gateway \ services/graph \ services/groups \ - services/guestauth \ services/idm \ services/idp \ services/invitations \ diff --git a/opencloud/pkg/command/services.go b/opencloud/pkg/command/services.go index 6364be5d7e..d16e78dba3 100644 --- a/opencloud/pkg/command/services.go +++ b/opencloud/pkg/command/services.go @@ -15,6 +15,7 @@ import ( 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" + authguest "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/command" authmachine "github.com/opencloud-eu/opencloud/services/auth-machine/pkg/command" authservice "github.com/opencloud-eu/opencloud/services/auth-service/pkg/command" clientlog "github.com/opencloud-eu/opencloud/services/clientlog/pkg/command" @@ -24,7 +25,6 @@ import ( gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/command" graph "github.com/opencloud-eu/opencloud/services/graph/pkg/command" groups "github.com/opencloud-eu/opencloud/services/groups/pkg/command" - guestauth "github.com/opencloud-eu/opencloud/services/guestauth/pkg/command" idm "github.com/opencloud-eu/opencloud/services/idm/pkg/command" idp "github.com/opencloud-eu/opencloud/services/idp/pkg/command" invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/command" @@ -140,8 +140,8 @@ var serviceCommands = []register.Command{ }) }, func(cfg *config.Config) *cobra.Command { - return ServiceCommand(cfg, cfg.GuestAuth.Service.Name, guestauth.GetCommands(cfg.GuestAuth), func(c *config.Config) { - cfg.GuestAuth.Commons = cfg.Commons + return ServiceCommand(cfg, cfg.AuthGuest.Service.Name, authguest.GetCommands(cfg.AuthGuest), func(c *config.Config) { + cfg.AuthGuest.Commons = cfg.Commons }) }, func(cfg *config.Config) *cobra.Command { diff --git a/opencloud/pkg/init/init.go b/opencloud/pkg/init/init.go index 511fafd293..4e50d396c5 100644 --- a/opencloud/pkg/init/init.go +++ b/opencloud/pkg/init/init.go @@ -69,7 +69,7 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword idmServicePassword, idpServicePassword, ocAdminServicePassword, revaServicePassword string tokenManagerJwtSecret, collaborationWOPISecret, machineAuthAPIKey, systemUserAPIKey string revaTransferSecret, thumbnailsTransferSecret, serviceAccountSecret, urlSigningSecret string - guestAuthJWTSecret string + authGuestJWTSecret string adminPasswdwordGenerated bool ) @@ -104,11 +104,11 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword return fmt.Errorf("could not generate random secret for urlSigningSecret: %s", err) } } - guestAuthJWTSecret = oldCfg.GuestAuth.TokenManager.JWTSecret - if guestAuthJWTSecret == "" { - guestAuthJWTSecret, err = generators.GenerateRandomPassword(passwordLength) + authGuestJWTSecret = oldCfg.AuthGuest.TokenManager.JWTSecret + if authGuestJWTSecret == "" { + authGuestJWTSecret, err = generators.GenerateRandomPassword(passwordLength) if err != nil { - return fmt.Errorf("could not generate random secret for guestAuthJWTSecret: %s", err) + return fmt.Errorf("could not generate random secret for authGuestJWTSecret: %s", err) } } } else { @@ -163,9 +163,9 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword if err != nil { return fmt.Errorf("could not generate random secret for urlSigningSecret: %s", err) } - guestAuthJWTSecret, err = generators.GenerateRandomPassword(passwordLength) + authGuestJWTSecret, err = generators.GenerateRandomPassword(passwordLength) if err != nil { - return fmt.Errorf("could not generate random secret for guestAuthJWTSecret: %s", err) + return fmt.Errorf("could not generate random secret for authGuestJWTSecret: %s", err) } thumbnailsTransferSecret, err = generators.GenerateRandomPassword(passwordLength) if err != nil { @@ -224,9 +224,9 @@ func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword }, }, }, - GuestAuth: GuestAuth{ + AuthGuest: AuthGuest{ ServiceAccount: serviceAccount, - TokenManager: TokenManager{JWTSecret: guestAuthJWTSecret}, + TokenManager: TokenManager{JWTSecret: authGuestJWTSecret}, }, Users: UsersAndGroupsService{ Drivers: LdapBasedService{ diff --git a/opencloud/pkg/init/structs.go b/opencloud/pkg/init/structs.go index c3362c1c0f..eec147250b 100644 --- a/opencloud/pkg/init/structs.go +++ b/opencloud/pkg/init/structs.go @@ -32,7 +32,7 @@ type OpenCloudConfig struct { AuthBearer AuthbearerService `yaml:"auth_bearer"` Users UsersAndGroupsService `yaml:"users"` Groups UsersAndGroupsService `yaml:"groups"` - GuestAuth GuestAuth `yaml:"guestauth"` + AuthGuest AuthGuest `yaml:"auth_guest"` Ocm OcmService `yaml:"ocm"` Thumbnails ThumbnailService `yaml:"thumbnails"` Search Search `yaml:"search"` @@ -54,8 +54,8 @@ type Activitylog struct { ServiceAccount ServiceAccount `yaml:"service_account"` } -// GuestAuth is the configuration for the guestauth service -type GuestAuth struct { +// AuthGuest is the configuration for the auth-guest service +type AuthGuest struct { ServiceAccount ServiceAccount `yaml:"service_account"` TokenManager TokenManager `yaml:"token_manager"` } diff --git a/opencloud/pkg/runtime/service/service.go b/opencloud/pkg/runtime/service/service.go index c002621209..432ef703ca 100644 --- a/opencloud/pkg/runtime/service/service.go +++ b/opencloud/pkg/runtime/service/service.go @@ -26,6 +26,7 @@ import ( audit "github.com/opencloud-eu/opencloud/services/audit/pkg/command" authapp "github.com/opencloud-eu/opencloud/services/auth-app/pkg/command" authbasic "github.com/opencloud-eu/opencloud/services/auth-basic/pkg/command" + authguest "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/command" authmachine "github.com/opencloud-eu/opencloud/services/auth-machine/pkg/command" authservice "github.com/opencloud-eu/opencloud/services/auth-service/pkg/command" clientlog "github.com/opencloud-eu/opencloud/services/clientlog/pkg/command" @@ -35,7 +36,6 @@ import ( gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/command" graph "github.com/opencloud-eu/opencloud/services/graph/pkg/command" groups "github.com/opencloud-eu/opencloud/services/groups/pkg/command" - guestauth "github.com/opencloud-eu/opencloud/services/guestauth/pkg/command" idm "github.com/opencloud-eu/opencloud/services/idm/pkg/command" idp "github.com/opencloud-eu/opencloud/services/idp/pkg/command" invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/command" @@ -199,10 +199,10 @@ func NewService(ctx context.Context, options ...Option) (*Service, error) { cfg.Groups.Commons = cfg.Commons return groups.Execute(cfg.Groups) }) - reg(3, opts.Config.GuestAuth.Service.Name, func(ctx context.Context, cfg *occfg.Config) error { - cfg.GuestAuth.Context = ctx - cfg.GuestAuth.Commons = cfg.Commons - return guestauth.Execute(cfg.GuestAuth) + reg(3, opts.Config.AuthGuest.Service.Name, func(ctx context.Context, cfg *occfg.Config) error { + cfg.AuthGuest.Context = ctx + cfg.AuthGuest.Commons = cfg.Commons + return authguest.Execute(cfg.AuthGuest) }) reg(3, opts.Config.IDM.Service.Name, func(ctx context.Context, cfg *occfg.Config) error { cfg.IDM.Context = ctx diff --git a/pkg/config/config.go b/pkg/config/config.go index 9cf8f91ed5..77f832d45f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -10,6 +10,7 @@ import ( 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" + authguest "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config" authmachine "github.com/opencloud-eu/opencloud/services/auth-machine/pkg/config" authservice "github.com/opencloud-eu/opencloud/services/auth-service/pkg/config" clientlog "github.com/opencloud-eu/opencloud/services/clientlog/pkg/config" @@ -19,7 +20,6 @@ import ( gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/config" graph "github.com/opencloud-eu/opencloud/services/graph/pkg/config" groups "github.com/opencloud-eu/opencloud/services/groups/pkg/config" - guestauth "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" idm "github.com/opencloud-eu/opencloud/services/idm/pkg/config" idp "github.com/opencloud-eu/opencloud/services/idp/pkg/config" invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/config" @@ -100,7 +100,7 @@ type Config struct { Gateway *gateway.Config `yaml:"gateway"` Graph *graph.Config `yaml:"graph"` Groups *groups.Config `yaml:"groups"` - GuestAuth *guestauth.Config `yaml:"guestauth"` + AuthGuest *authguest.Config `yaml:"auth_guest"` IDM *idm.Config `yaml:"idm"` IDP *idp.Config `yaml:"idp"` Invitations *invitations.Config `yaml:"invitations"` diff --git a/pkg/config/defaultconfig.go b/pkg/config/defaultconfig.go index 392184b2ee..586b5cd296 100644 --- a/pkg/config/defaultconfig.go +++ b/pkg/config/defaultconfig.go @@ -10,6 +10,7 @@ import ( 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" + authguest "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config/defaults" authmachine "github.com/opencloud-eu/opencloud/services/auth-machine/pkg/config/defaults" authservice "github.com/opencloud-eu/opencloud/services/auth-service/pkg/config/defaults" clientlog "github.com/opencloud-eu/opencloud/services/clientlog/pkg/config/defaults" @@ -19,7 +20,6 @@ import ( gateway "github.com/opencloud-eu/opencloud/services/gateway/pkg/config/defaults" graph "github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults" groups "github.com/opencloud-eu/opencloud/services/groups/pkg/config/defaults" - guestauth "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config/defaults" idm "github.com/opencloud-eu/opencloud/services/idm/pkg/config/defaults" idp "github.com/opencloud-eu/opencloud/services/idp/pkg/config/defaults" invitations "github.com/opencloud-eu/opencloud/services/invitations/pkg/config/defaults" @@ -75,7 +75,7 @@ func DefaultConfig() *Config { Gateway: gateway.DefaultConfig(), Graph: graph.DefaultConfig(), Groups: groups.DefaultConfig(), - GuestAuth: guestauth.DefaultConfig(), + AuthGuest: authguest.DefaultConfig(), IDM: idm.DefaultConfig(), IDP: idp.DefaultConfig(), Invitations: invitations.DefaultConfig(), diff --git a/services/guestauth/.mockery.yaml b/services/auth-guest/.mockery.yaml similarity index 57% rename from services/guestauth/.mockery.yaml rename to services/auth-guest/.mockery.yaml index 53f85a5896..e290a57cf6 100644 --- a/services/guestauth/.mockery.yaml +++ b/services/auth-guest/.mockery.yaml @@ -6,9 +6,9 @@ pkgname: mocks template: testify packages: - github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth: + github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest: interfaces: - GuestAuth: {} - github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage: + AuthGuest: {} + github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/storage: interfaces: Manager: {} diff --git a/services/guestauth/Makefile b/services/auth-guest/Makefile similarity index 94% rename from services/guestauth/Makefile rename to services/auth-guest/Makefile index 9a191622a7..bb440dd6dc 100644 --- a/services/guestauth/Makefile +++ b/services/auth-guest/Makefile @@ -1,5 +1,5 @@ SHELL := bash -NAME := guestauth +NAME := auth-guest ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI include ../../.bingo/Variables.mk diff --git a/services/guestauth/README.md b/services/auth-guest/README.md similarity index 69% rename from services/guestauth/README.md rename to services/auth-guest/README.md index 196b9b541a..167cc41fa4 100644 --- a/services/guestauth/README.md +++ b/services/auth-guest/README.md @@ -1,6 +1,6 @@ -# Guestauth +# auth-guest -The `guestauth` service gives guest users access to a share without a full +The `auth-guest` service gives guest users access to a share without a full OpenCloud account. When a share is created for a user of type `USER_TYPE_GUEST`, the service issues a one-time invitation token; redeeming that token exchanges it for a signed session cookie that authenticates the @@ -35,19 +35,19 @@ It is part of the default service set and does not need to be enabled with ## Configuration -The service is configured via `GUESTAUTH_*` environment variables or a -`guestauth.yaml` file. +The service is configured via `AUTH_GUEST_*` environment variables or a +`auth-guest.yaml` file. -To run only the HTTP part, set `GUESTAUTH_EVENTS_DISABLED=true`. To run only -the event consumer, set `GUESTAUTH_HTTP_DISABLED=true`. +To run only the HTTP part, set `AUTH_GUEST_EVENTS_DISABLED=true`. To run only +the event consumer, set `AUTH_GUEST_HTTP_DISABLED=true`. Relevant options: -- `GUESTAUTH_JWT_SECRET` — secret used to sign session tokens. -- `GUESTAUTH_JWT_COOKIE_NAME`, `GUESTAUTH_JWT_TTL` — session cookie name and +- `AUTH_GUEST_JWT_SECRET` — secret used to sign session tokens. +- `AUTH_GUEST_JWT_COOKIE_NAME`, `AUTH_GUEST_JWT_TTL` — session cookie name and lifetime. -- `GUESTAUTH_TOKENS_STORAGE_ROOT` — where invitation token records are stored. -- `GUESTAUTH_SERVICE_ACCOUNT_ID`, `GUESTAUTH_SERVICE_ACCOUNT_SECRET` — service +- `AUTH_GUEST_TOKENS_STORAGE_ROOT` — where invitation token records are stored. +- `AUTH_GUEST_SERVICE_ACCOUNT_ID`, `AUTH_GUEST_SERVICE_ACCOUNT_SECRET` — service account used to query the gateway for share metadata. -- `GUESTAUTH_NUM_CONSUMERS` — number of concurrent event consumers. +- `AUTH_GUEST_NUM_CONSUMERS` — number of concurrent event consumers. - `OC_REVA_GATEWAY` — CS3 gateway used to look up shares. diff --git a/services/guestauth/pkg/command/health.go b/services/auth-guest/pkg/command/health.go similarity index 67% rename from services/guestauth/pkg/command/health.go rename to services/auth-guest/pkg/command/health.go index 31dfe507ad..6825fe20a0 100644 --- a/services/guestauth/pkg/command/health.go +++ b/services/auth-guest/pkg/command/health.go @@ -1,7 +1,10 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package command import ( - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config" "github.com/spf13/cobra" ) diff --git a/services/guestauth/pkg/command/root.go b/services/auth-guest/pkg/command/root.go similarity index 67% rename from services/guestauth/pkg/command/root.go rename to services/auth-guest/pkg/command/root.go index 775abdf7a0..114a5a407a 100644 --- a/services/guestauth/pkg/command/root.go +++ b/services/auth-guest/pkg/command/root.go @@ -1,10 +1,13 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package command import ( "os" "github.com/opencloud-eu/opencloud/pkg/clihelper" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config" "github.com/spf13/cobra" ) @@ -22,11 +25,11 @@ func GetCommands(cfg *config.Config) []*cobra.Command { } } -// Execute is the entry point for the guestauth command. +// Execute is the entry point for the auth-guest command. func Execute(cfg *config.Config) error { app := clihelper.DefaultApp(&cobra.Command{ - Use: "guestauth", - Short: "starts guestauth service", + Use: "auth-guest", + Short: "starts auth-guest service", }) app.AddCommand(GetCommands(cfg)...) app.SetArgs(os.Args[1:]) diff --git a/services/guestauth/pkg/command/server.go b/services/auth-guest/pkg/command/server.go similarity index 80% rename from services/guestauth/pkg/command/server.go rename to services/auth-guest/pkg/command/server.go index c8fc5cccba..204ec6b290 100644 --- a/services/guestauth/pkg/command/server.go +++ b/services/auth-guest/pkg/command/server.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package command import ( @@ -13,16 +16,16 @@ import ( "github.com/opencloud-eu/opencloud/pkg/runner" "github.com/opencloud-eu/opencloud/pkg/tracing" "github.com/opencloud-eu/opencloud/pkg/version" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config/parser" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/metrics" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/server/debug" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/server/http" - svcEvents "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/events" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/jwt" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config/parser" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/metrics" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/server/debug" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/server/http" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest" + svcEvents "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/events" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/jwt" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/storage" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/token" "github.com/opencloud-eu/reva/v2/pkg/events" "github.com/opencloud-eu/reva/v2/pkg/events/stream" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" @@ -76,10 +79,10 @@ func Server(cfg *config.Config) *cobra.Command { store := storage.NewFileManager(cfg.Storage.RootDirectory) jwtService := jwt.NewJwtService(cfg.TokenManager.JWTSecret, cfg.JWT.TTL) - guestAuth := guestauth.NewGuestAuthService(tokenSvc, store, - guestauth.GatewaySelector(gatewaySelector), - guestauth.ServiceAccount(cfg.ServiceAccount), - guestauth.JWT(jwtService), + authGuest := authguest.NewAuthGuestService(tokenSvc, store, + authguest.GatewaySelector(gatewaySelector), + authguest.ServiceAccount(cfg.ServiceAccount), + authguest.JWT(jwtService), ) if !cfg.HTTP.Disabled { @@ -87,7 +90,7 @@ func Server(cfg *config.Config) *cobra.Command { http.Logger(logger), http.Context(ctx), http.Config(cfg), - http.Service(guestAuth), + http.Service(authGuest), ) if err != nil { logger.Info(). @@ -125,7 +128,7 @@ func Server(cfg *config.Config) *cobra.Command { svcEvents.Context(ctx), svcEvents.RegisteredEvents(_registeredEvents), svcEvents.NumConsumers(cfg.NumConsumers), - svcEvents.GuestAuthService(guestAuth), + svcEvents.AuthGuestService(authGuest), ) if err != nil { logger.Error().Err(err).Str("transport", "event").Msg("Failed to initialize server") diff --git a/services/guestauth/pkg/command/version.go b/services/auth-guest/pkg/command/version.go similarity index 71% rename from services/guestauth/pkg/command/version.go rename to services/auth-guest/pkg/command/version.go index 1debe78edb..228f6ae325 100644 --- a/services/guestauth/pkg/command/version.go +++ b/services/auth-guest/pkg/command/version.go @@ -1,7 +1,10 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package command import ( - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config" "github.com/spf13/cobra" ) diff --git a/services/auth-guest/pkg/config/config.go b/services/auth-guest/pkg/config/config.go new file mode 100644 index 0000000000..1e24494681 --- /dev/null +++ b/services/auth-guest/pkg/config/config.go @@ -0,0 +1,90 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "context" + "time" + + "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:"-"` + + LogLevel string `yaml:"loglevel" env:"OC_LOG_LEVEL;AUTH_GUEST_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"%%NEXT%%"` + + Debug Debug `yaml:"debug"` + + Events Events `yaml:"events"` + + RevaGateway string `yaml:"reva_gateway" env:"OC_REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata" introductionVersion:"%%NEXT%%"` + GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"` + + HTTP HTTP `yaml:"http"` + Storage Storage `yaml:"storage"` + TokenManager *TokenManager `yaml:"token_manager"` + JWT JWT `yaml:"jwt"` + + ServiceAccount ServiceAccount `yaml:"service_account"` + + NumConsumers int `yaml:"num_consumers" env:"AUTH_GUEST_NUM_CONSUMERS" desc:"The amount of concurrent event consumers to start. Event consumers are used for processing events. Multiple consumers increase parallelisation, but will also increase CPU and memory demands." introductionVersion:"%%NEXT%%"` + + Context context.Context `yaml:"-"` +} + +// Events combines the configuration options for the event bus. +type Events struct { + Disabled bool `yaml:"disabled" env:"AUTH_GUEST_EVENTS_DISABLED" desc:"Disables listening for events. Set this to true if the service should only handle HTTP requests." introductionVersion:"%%NEXT%%"` + Endpoint string `yaml:"endpoint" env:"OC_EVENTS_ENDPOINT" desc:"The address of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture." introductionVersion:"%%NEXT%%"` + Cluster string `yaml:"cluster" env:"OC_EVENTS_CLUSTER" desc:"The clusterID of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture. Mandatory when using NATS as event system." introductionVersion:"%%NEXT%%"` + TLSInsecure bool `yaml:"tls_insecure" env:"OC_INSECURE;OC_EVENTS_TLS_INSECURE" desc:"Whether to verify the server TLS certificates." introductionVersion:"%%NEXT%%"` + TLSRootCACertificate string `yaml:"tls_root_ca_certificate" env:"OC_EVENTS_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided AUTH_GUEST_EVENTS_TLS_INSECURE will be seen as false." introductionVersion:"%%NEXT%%"` + EnableTLS bool `yaml:"enable_tls" env:"OC_EVENTS_ENABLE_TLS" desc:"Enable TLS for the connection to the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"%%NEXT%%"` + AuthUsername string `yaml:"username" env:"OC_EVENTS_AUTH_USERNAME" desc:"The username to authenticate with the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"%%NEXT%%"` + AuthPassword string `yaml:"password" env:"OC_EVENTS_AUTH_PASSWORD" desc:"The password to authenticate with the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"%%NEXT%%"` +} + +// ServiceAccount is the configuration for the used service account +type ServiceAccount struct { + ServiceAccountID string `yaml:"service_account_id" env:"OC_SERVICE_ACCOUNT_ID;AUTH_GUEST_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details." introductionVersion:"%%NEXT%%"` + ServiceAccountSecret string `yaml:"service_account_secret" env:"OC_SERVICE_ACCOUNT_SECRET;AUTH_GUEST_SERVICE_ACCOUNT_SECRET" desc:"The service account secret." introductionVersion:"%%NEXT%%"` +} + +// CORS defines the available cors configuration. +type CORS struct { + AllowedOrigins []string `yaml:"allow_origins" env:"OC_CORS_ALLOW_ORIGINS;AUTH_GUEST_CORS_ALLOW_ORIGINS" desc:"A 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. See the Environment Variable Types description for more details." introductionVersion:"%%NEXT%%"` + AllowedMethods []string `yaml:"allow_methods" env:"OC_CORS_ALLOW_METHODS;AUTH_GUEST_CORS_ALLOW_METHODS" desc:"A 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. See the Environment Variable Types description for more details." introductionVersion:"%%NEXT%%"` + AllowedHeaders []string `yaml:"allow_headers" env:"OC_CORS_ALLOW_HEADERS;AUTH_GUEST_CORS_ALLOW_HEADERS" desc:"A 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. See the Environment Variable Types description for more details." introductionVersion:"%%NEXT%%"` + AllowCredentials bool `yaml:"allow_credentials" env:"OC_CORS_ALLOW_CREDENTIALS;AUTH_GUEST_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." introductionVersion:"%%NEXT%%"` +} + +// HTTP defines the available http configuration. +type HTTP struct { + Disabled bool `yaml:"disabled" env:"AUTH_GUEST_HTTP_DISABLED" desc:"Disables the HTTP service. Set this to true if the service should only handle events." introductionVersion:"%%NEXT%%"` + Addr string `yaml:"addr" env:"AUTH_GUEST_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"%%NEXT%%"` + Namespace string `yaml:"-"` + Root string `yaml:"root" env:"AUTH_GUEST_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"%%NEXT%%"` + CORS CORS `yaml:"cors"` + TLS shared.HTTPServiceTLS `yaml:"tls"` +} + +// Storage defines the configuration for the token storage. +type Storage struct { + RootDirectory string `yaml:"root_directory" env:"AUTH_GUEST_TOKENS_STORAGE_ROOT" desc:"The directory where the guest share tokens are stored. If not defined, the root directory derives from $OC_BASE_DATA_PATH/auth-guest." introductionVersion:"%%NEXT%%"` +} + +// TokenManager is the config for using the reva token manager +type TokenManager struct { + JWTSecret string `yaml:"jwt_secret" env:"AUTH_GUEST_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"%%NEXT%%"` +} + +// JWT defines the configuration for guest session tokens. +type JWT struct { + CookieName string `yaml:"cookie_name" env:"AUTH_GUEST_JWT_COOKIE_NAME" desc:"The name of the session cookie set when a guest token is redeemed." introductionVersion:"%%NEXT%%"` + TTL time.Duration `yaml:"ttl" env:"AUTH_GUEST_JWT_TTL" desc:"The lifetime of a redeemed guest session token." introductionVersion:"%%NEXT%%"` +} diff --git a/services/auth-guest/pkg/config/debug.go b/services/auth-guest/pkg/config/debug.go new file mode 100644 index 0000000000..79013cf436 --- /dev/null +++ b/services/auth-guest/pkg/config/debug.go @@ -0,0 +1,12 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + +package config + +// Debug defines the available debug configuration. +type Debug struct { + Addr string `yaml:"addr" env:"AUTH_GUEST_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"%%NEXT%%"` + Token string `yaml:"token" env:"AUTH_GUEST_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"%%NEXT%%"` + Pprof bool `yaml:"pprof" env:"AUTH_GUEST_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"%%NEXT%%"` + Zpages bool `yaml:"zpages" env:"AUTH_GUEST_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"%%NEXT%%"` +} diff --git a/services/guestauth/pkg/config/defaults/defaultconfig.go b/services/auth-guest/pkg/config/defaults/defaultconfig.go similarity index 88% rename from services/guestauth/pkg/config/defaults/defaultconfig.go rename to services/auth-guest/pkg/config/defaults/defaultconfig.go index fe2737e2ab..517047db04 100644 --- a/services/guestauth/pkg/config/defaults/defaultconfig.go +++ b/services/auth-guest/pkg/config/defaults/defaultconfig.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package defaults import ( @@ -7,7 +10,7 @@ import ( "github.com/opencloud-eu/opencloud/pkg/config/defaults" "github.com/opencloud-eu/opencloud/pkg/shared" "github.com/opencloud-eu/opencloud/pkg/structs" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config" ) // FullDefaultConfig returns the full default config @@ -28,7 +31,7 @@ func DefaultConfig() *config.Config { Zpages: false, }, Service: config.Service{ - Name: "guestauth", + Name: "auth-guest", }, NumConsumers: 1, Events: config.Events{ @@ -49,7 +52,7 @@ func DefaultConfig() *config.Config { }, }, Storage: config.Storage{ - RootDirectory: path.Join(defaults.BaseDataPath(), "guestauth"), + RootDirectory: path.Join(defaults.BaseDataPath(), "auth-guest"), }, JWT: config.JWT{ CookieName: "oc_guest_session", diff --git a/services/guestauth/pkg/config/parser/parse.go b/services/auth-guest/pkg/config/parser/parse.go similarity index 80% rename from services/guestauth/pkg/config/parser/parse.go rename to services/auth-guest/pkg/config/parser/parse.go index ff036e858f..b2add76af3 100644 --- a/services/guestauth/pkg/config/parser/parse.go +++ b/services/auth-guest/pkg/config/parser/parse.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package parser import ( @@ -5,8 +8,8 @@ import ( occfg "github.com/opencloud-eu/opencloud/pkg/config" "github.com/opencloud-eu/opencloud/pkg/shared" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config/defaults" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config/defaults" "github.com/opencloud-eu/opencloud/pkg/config/envdecode" ) diff --git a/services/guestauth/pkg/config/service.go b/services/auth-guest/pkg/config/service.go similarity index 56% rename from services/guestauth/pkg/config/service.go rename to services/auth-guest/pkg/config/service.go index d1eac383f0..1716901eff 100644 --- a/services/guestauth/pkg/config/service.go +++ b/services/auth-guest/pkg/config/service.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package config // Service defines the available service configuration. diff --git a/services/guestauth/pkg/metrics/metrics.go b/services/auth-guest/pkg/metrics/metrics.go similarity index 85% rename from services/guestauth/pkg/metrics/metrics.go rename to services/auth-guest/pkg/metrics/metrics.go index d4f167cc33..3f79e38f7c 100644 --- a/services/guestauth/pkg/metrics/metrics.go +++ b/services/auth-guest/pkg/metrics/metrics.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package metrics import "github.com/prometheus/client_golang/prometheus" @@ -7,7 +10,7 @@ var ( Namespace = "opencloud" // Subsystem defines the subsystem for the defines metrics. - Subsystem = "guestauth" + Subsystem = "auth-guest" ) // Metrics defines the available metrics of this service. diff --git a/services/guestauth/pkg/server/debug/option.go b/services/auth-guest/pkg/server/debug/option.go similarity index 85% rename from services/guestauth/pkg/server/debug/option.go rename to services/auth-guest/pkg/server/debug/option.go index 3f11ec3e46..bf9b9e16d0 100644 --- a/services/guestauth/pkg/server/debug/option.go +++ b/services/auth-guest/pkg/server/debug/option.go @@ -1,10 +1,13 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package debug import ( "context" "github.com/opencloud-eu/opencloud/pkg/log" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config" ) // Option defines a single option function. diff --git a/services/guestauth/pkg/server/debug/server.go b/services/auth-guest/pkg/server/debug/server.go similarity index 93% rename from services/guestauth/pkg/server/debug/server.go rename to services/auth-guest/pkg/server/debug/server.go index 79a016f421..5b6b9ef8ee 100644 --- a/services/guestauth/pkg/server/debug/server.go +++ b/services/auth-guest/pkg/server/debug/server.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package debug import ( diff --git a/services/guestauth/pkg/server/http/errors.go b/services/auth-guest/pkg/server/http/errors.go similarity index 72% rename from services/guestauth/pkg/server/http/errors.go rename to services/auth-guest/pkg/server/http/errors.go index 14b542e24c..95834fac62 100644 --- a/services/guestauth/pkg/server/http/errors.go +++ b/services/auth-guest/pkg/server/http/errors.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package http import ( @@ -5,9 +8,9 @@ import ( "errors" "net/http" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/storage" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/token" ) type errorResponse struct { @@ -23,7 +26,7 @@ func writeError(w http.ResponseWriter, status int, body errorResponse) { } func writeRedeemError(w http.ResponseWriter, err error) { - var re *guestauth.RedeemError + var re *authguest.RedeemError if !errors.As(err, &re) { writeError(w, http.StatusInternalServerError, errorResponse{ErrorType: "internal_error", Message: "An internal error occurred."}) return @@ -32,7 +35,7 @@ func writeRedeemError(w http.ResponseWriter, err error) { status := http.StatusInternalServerError errorType := "internal_error" switch { - case errors.Is(re.ErrorType, guestauth.ErrExpired): + case errors.Is(re.ErrorType, authguest.ErrExpired): status, errorType = http.StatusUnauthorized, "token_expired" case errors.Is(re.ErrorType, token.ErrInvalidToken): status, errorType = http.StatusUnauthorized, "token_invalid" @@ -40,11 +43,11 @@ func writeRedeemError(w http.ResponseWriter, err error) { status, errorType = http.StatusNotFound, "token_not_found" case errors.Is(re.ErrorType, storage.ErrInvalidHash): status, errorType = http.StatusUnauthorized, "token_invalid" - case errors.Is(re.ErrorType, guestauth.ErrAlreadyRedeemed): + case errors.Is(re.ErrorType, authguest.ErrAlreadyRedeemed): status, errorType = http.StatusConflict, "token_already_redeemed" - case errors.Is(re.ErrorType, guestauth.ErrShareNotFound): + case errors.Is(re.ErrorType, authguest.ErrShareNotFound): status, errorType = http.StatusNotFound, "share_not_found" - case errors.Is(re.ErrorType, guestauth.ErrShareExpired): + case errors.Is(re.ErrorType, authguest.ErrShareExpired): status, errorType = http.StatusGone, "share_expired" } diff --git a/services/guestauth/pkg/server/http/option.go b/services/auth-guest/pkg/server/http/option.go similarity index 79% rename from services/guestauth/pkg/server/http/option.go rename to services/auth-guest/pkg/server/http/option.go index 5c5c54ac51..fc4332ca8b 100644 --- a/services/guestauth/pkg/server/http/option.go +++ b/services/auth-guest/pkg/server/http/option.go @@ -1,11 +1,14 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package http import ( "context" "github.com/opencloud-eu/opencloud/pkg/log" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest" "github.com/spf13/pflag" ) @@ -17,7 +20,7 @@ type Options struct { Logger log.Logger Context context.Context Config *config.Config - Service guestauth.GuestAuth + Service authguest.AuthGuest Flags []pflag.Flag } @@ -54,7 +57,7 @@ func Config(val *config.Config) Option { } // Service provides a function to set the service option. -func Service(val guestauth.GuestAuth) Option { +func Service(val authguest.AuthGuest) Option { return func(o *Options) { o.Service = val } diff --git a/services/guestauth/pkg/server/http/redeem.go b/services/auth-guest/pkg/server/http/redeem.go similarity index 78% rename from services/guestauth/pkg/server/http/redeem.go rename to services/auth-guest/pkg/server/http/redeem.go index 78831017ea..3102a89f07 100644 --- a/services/guestauth/pkg/server/http/redeem.go +++ b/services/auth-guest/pkg/server/http/redeem.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package http import ( @@ -5,8 +8,8 @@ import ( "net/http" "github.com/opencloud-eu/opencloud/pkg/log" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest" ) // RedeemRequest is the request body for token redemption. @@ -15,7 +18,7 @@ type RedeemRequest struct { } // RedeemHandler validates the token submitted to the redeem endpoint. -func RedeemHandler(log log.Logger, s guestauth.GuestAuth, cfg *config.Config) func(w http.ResponseWriter, r *http.Request) { +func RedeemHandler(log log.Logger, s authguest.AuthGuest, cfg *config.Config) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var req RedeemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { diff --git a/services/guestauth/pkg/server/http/redeem_test.go b/services/auth-guest/pkg/server/http/redeem_test.go similarity index 74% rename from services/guestauth/pkg/server/http/redeem_test.go rename to services/auth-guest/pkg/server/http/redeem_test.go index cdae0c5eb2..c5579a0519 100644 --- a/services/guestauth/pkg/server/http/redeem_test.go +++ b/services/auth-guest/pkg/server/http/redeem_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package http import ( @@ -9,17 +12,17 @@ import ( "time" "github.com/opencloud-eu/opencloud/pkg/log" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth/mocks" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest/mocks" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/storage" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/token" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) -func newRedeemHandler(t *testing.T, svc guestauth.GuestAuth) http.HandlerFunc { +func newRedeemHandler(t *testing.T, svc authguest.AuthGuest) http.HandlerFunc { t.Helper() cfg := &config.Config{ JWT: config.JWT{ @@ -31,7 +34,7 @@ func newRedeemHandler(t *testing.T, svc guestauth.GuestAuth) http.HandlerFunc { } func TestRedeemHandler(t *testing.T) { - svcMock := mocks.NewGuestAuth(t) + svcMock := mocks.NewAuthGuest(t) svcMock.On("Redeem", mock.Anything, "valid-token").Return("session-token", nil) body, err := json.Marshal(RedeemRequest{Token: "valid-token"}) @@ -64,38 +67,38 @@ func TestRedeemHandlerErrorMapping(t *testing.T) { }{ { name: "token expired", - err: &guestauth.RedeemError{ErrorType: guestauth.ErrExpired, ShareID: "share-1"}, + err: &authguest.RedeemError{ErrorType: authguest.ErrExpired, ShareID: "share-1"}, wantStatus: http.StatusUnauthorized, wantType: "token_expired", wantShare: "share-1", }, { name: "token invalid", - err: &guestauth.RedeemError{ErrorType: token.ErrInvalidToken}, + err: &authguest.RedeemError{ErrorType: token.ErrInvalidToken}, wantStatus: http.StatusUnauthorized, wantType: "token_invalid", }, { name: "token not found", - err: &guestauth.RedeemError{ErrorType: storage.ErrNotFound}, + err: &authguest.RedeemError{ErrorType: storage.ErrNotFound}, wantStatus: http.StatusNotFound, wantType: "token_not_found", }, { name: "token already redeemed", - err: &guestauth.RedeemError{ErrorType: guestauth.ErrAlreadyRedeemed}, + err: &authguest.RedeemError{ErrorType: authguest.ErrAlreadyRedeemed}, wantStatus: http.StatusConflict, wantType: "token_already_redeemed", }, { name: "share not found", - err: &guestauth.RedeemError{ErrorType: guestauth.ErrShareNotFound}, + err: &authguest.RedeemError{ErrorType: authguest.ErrShareNotFound}, wantStatus: http.StatusNotFound, wantType: "share_not_found", }, { name: "share expired", - err: &guestauth.RedeemError{ErrorType: guestauth.ErrShareExpired}, + err: &authguest.RedeemError{ErrorType: authguest.ErrShareExpired}, wantStatus: http.StatusGone, wantType: "share_expired", }, @@ -103,7 +106,7 @@ func TestRedeemHandlerErrorMapping(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - svcMock := mocks.NewGuestAuth(t) + svcMock := mocks.NewAuthGuest(t) svcMock.On("Redeem", mock.Anything, "token").Return("", tt.err) body, err := json.Marshal(RedeemRequest{Token: "token"}) @@ -123,7 +126,7 @@ func TestRedeemHandlerErrorMapping(t *testing.T) { } func TestRedeemHandlerMalformedBody(t *testing.T) { - svcMock := mocks.NewGuestAuth(t) + svcMock := mocks.NewAuthGuest(t) rr := httptest.NewRecorder() newRedeemHandler(t, svcMock)(rr, httptest.NewRequest(http.MethodPost, "/", strings.NewReader("not-json"))) diff --git a/services/guestauth/pkg/server/http/server.go b/services/auth-guest/pkg/server/http/server.go similarity index 95% rename from services/guestauth/pkg/server/http/server.go rename to services/auth-guest/pkg/server/http/server.go index 8f7d169d93..6624523ff4 100644 --- a/services/guestauth/pkg/server/http/server.go +++ b/services/auth-guest/pkg/server/http/server.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package http import ( diff --git a/services/guestauth/pkg/service/guestauth/mocks/guest_auth.go b/services/auth-guest/pkg/service/authguest/mocks/auth_guest.go similarity index 57% rename from services/guestauth/pkg/service/guestauth/mocks/guest_auth.go rename to services/auth-guest/pkg/service/authguest/mocks/auth_guest.go index eb39880608..e917ba9035 100644 --- a/services/guestauth/pkg/service/guestauth/mocks/guest_auth.go +++ b/services/auth-guest/pkg/service/authguest/mocks/auth_guest.go @@ -7,17 +7,17 @@ package mocks import ( "context" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/token" mock "github.com/stretchr/testify/mock" ) -// NewGuestAuth creates a new instance of GuestAuth. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// NewAuthGuest creates a new instance of AuthGuest. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func NewGuestAuth(t interface { +func NewAuthGuest(t interface { mock.TestingT Cleanup(func()) -}) *GuestAuth { - mock := &GuestAuth{} +}) *AuthGuest { + mock := &AuthGuest{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) @@ -25,21 +25,21 @@ func NewGuestAuth(t interface { return mock } -// GuestAuth is an autogenerated mock type for the GuestAuth type -type GuestAuth struct { +// AuthGuest is an autogenerated mock type for the AuthGuest type +type AuthGuest struct { mock.Mock } -type GuestAuth_Expecter struct { +type AuthGuest_Expecter struct { mock *mock.Mock } -func (_m *GuestAuth) EXPECT() *GuestAuth_Expecter { - return &GuestAuth_Expecter{mock: &_m.Mock} +func (_m *AuthGuest) EXPECT() *AuthGuest_Expecter { + return &AuthGuest_Expecter{mock: &_m.Mock} } -// CleanupShare provides a mock function for the type GuestAuth -func (_mock *GuestAuth) CleanupShare(shareID string) error { +// CleanupShare provides a mock function for the type AuthGuest +func (_mock *AuthGuest) CleanupShare(shareID string) error { ret := _mock.Called(shareID) if len(ret) == 0 { @@ -55,18 +55,18 @@ func (_mock *GuestAuth) CleanupShare(shareID string) error { return r0 } -// GuestAuth_CleanupShare_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CleanupShare' -type GuestAuth_CleanupShare_Call struct { +// AuthGuest_CleanupShare_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CleanupShare' +type AuthGuest_CleanupShare_Call struct { *mock.Call } // CleanupShare is a helper method to define mock.On call // - shareID string -func (_e *GuestAuth_Expecter) CleanupShare(shareID any) *GuestAuth_CleanupShare_Call { - return &GuestAuth_CleanupShare_Call{Call: _e.mock.On("CleanupShare", shareID)} +func (_e *AuthGuest_Expecter) CleanupShare(shareID any) *AuthGuest_CleanupShare_Call { + return &AuthGuest_CleanupShare_Call{Call: _e.mock.On("CleanupShare", shareID)} } -func (_c *GuestAuth_CleanupShare_Call) Run(run func(shareID string)) *GuestAuth_CleanupShare_Call { +func (_c *AuthGuest_CleanupShare_Call) Run(run func(shareID string)) *AuthGuest_CleanupShare_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { @@ -79,18 +79,18 @@ func (_c *GuestAuth_CleanupShare_Call) Run(run func(shareID string)) *GuestAuth_ return _c } -func (_c *GuestAuth_CleanupShare_Call) Return(err error) *GuestAuth_CleanupShare_Call { +func (_c *AuthGuest_CleanupShare_Call) Return(err error) *AuthGuest_CleanupShare_Call { _c.Call.Return(err) return _c } -func (_c *GuestAuth_CleanupShare_Call) RunAndReturn(run func(shareID string) error) *GuestAuth_CleanupShare_Call { +func (_c *AuthGuest_CleanupShare_Call) RunAndReturn(run func(shareID string) error) *AuthGuest_CleanupShare_Call { _c.Call.Return(run) return _c } -// CreateToken provides a mock function for the type GuestAuth -func (_mock *GuestAuth) CreateToken(ctx context.Context, shareID string) (*token.Token, error) { +// CreateToken provides a mock function for the type AuthGuest +func (_mock *AuthGuest) CreateToken(ctx context.Context, shareID string) (*token.Token, error) { ret := _mock.Called(ctx, shareID) if len(ret) == 0 { @@ -117,19 +117,19 @@ func (_mock *GuestAuth) CreateToken(ctx context.Context, shareID string) (*token return r0, r1 } -// GuestAuth_CreateToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateToken' -type GuestAuth_CreateToken_Call struct { +// AuthGuest_CreateToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateToken' +type AuthGuest_CreateToken_Call struct { *mock.Call } // CreateToken is a helper method to define mock.On call // - ctx context.Context // - shareID string -func (_e *GuestAuth_Expecter) CreateToken(ctx any, shareID any) *GuestAuth_CreateToken_Call { - return &GuestAuth_CreateToken_Call{Call: _e.mock.On("CreateToken", ctx, shareID)} +func (_e *AuthGuest_Expecter) CreateToken(ctx any, shareID any) *AuthGuest_CreateToken_Call { + return &AuthGuest_CreateToken_Call{Call: _e.mock.On("CreateToken", ctx, shareID)} } -func (_c *GuestAuth_CreateToken_Call) Run(run func(ctx context.Context, shareID string)) *GuestAuth_CreateToken_Call { +func (_c *AuthGuest_CreateToken_Call) Run(run func(ctx context.Context, shareID string)) *AuthGuest_CreateToken_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -147,18 +147,18 @@ func (_c *GuestAuth_CreateToken_Call) Run(run func(ctx context.Context, shareID return _c } -func (_c *GuestAuth_CreateToken_Call) Return(token1 *token.Token, err error) *GuestAuth_CreateToken_Call { +func (_c *AuthGuest_CreateToken_Call) Return(token1 *token.Token, err error) *AuthGuest_CreateToken_Call { _c.Call.Return(token1, err) return _c } -func (_c *GuestAuth_CreateToken_Call) RunAndReturn(run func(ctx context.Context, shareID string) (*token.Token, error)) *GuestAuth_CreateToken_Call { +func (_c *AuthGuest_CreateToken_Call) RunAndReturn(run func(ctx context.Context, shareID string) (*token.Token, error)) *AuthGuest_CreateToken_Call { _c.Call.Return(run) return _c } -// Redeem provides a mock function for the type GuestAuth -func (_mock *GuestAuth) Redeem(ctx context.Context, tokenString string) (string, error) { +// Redeem provides a mock function for the type AuthGuest +func (_mock *AuthGuest) Redeem(ctx context.Context, tokenString string) (string, error) { ret := _mock.Called(ctx, tokenString) if len(ret) == 0 { @@ -183,19 +183,19 @@ func (_mock *GuestAuth) Redeem(ctx context.Context, tokenString string) (string, return r0, r1 } -// GuestAuth_Redeem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Redeem' -type GuestAuth_Redeem_Call struct { +// AuthGuest_Redeem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Redeem' +type AuthGuest_Redeem_Call struct { *mock.Call } // Redeem is a helper method to define mock.On call // - ctx context.Context // - tokenString string -func (_e *GuestAuth_Expecter) Redeem(ctx any, tokenString any) *GuestAuth_Redeem_Call { - return &GuestAuth_Redeem_Call{Call: _e.mock.On("Redeem", ctx, tokenString)} +func (_e *AuthGuest_Expecter) Redeem(ctx any, tokenString any) *AuthGuest_Redeem_Call { + return &AuthGuest_Redeem_Call{Call: _e.mock.On("Redeem", ctx, tokenString)} } -func (_c *GuestAuth_Redeem_Call) Run(run func(ctx context.Context, tokenString string)) *GuestAuth_Redeem_Call { +func (_c *AuthGuest_Redeem_Call) Run(run func(ctx context.Context, tokenString string)) *AuthGuest_Redeem_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -213,12 +213,12 @@ func (_c *GuestAuth_Redeem_Call) Run(run func(ctx context.Context, tokenString s return _c } -func (_c *GuestAuth_Redeem_Call) Return(s string, err error) *GuestAuth_Redeem_Call { +func (_c *AuthGuest_Redeem_Call) Return(s string, err error) *AuthGuest_Redeem_Call { _c.Call.Return(s, err) return _c } -func (_c *GuestAuth_Redeem_Call) RunAndReturn(run func(ctx context.Context, tokenString string) (string, error)) *GuestAuth_Redeem_Call { +func (_c *AuthGuest_Redeem_Call) RunAndReturn(run func(ctx context.Context, tokenString string) (string, error)) *AuthGuest_Redeem_Call { _c.Call.Return(run) return _c } diff --git a/services/guestauth/pkg/service/guestauth/options.go b/services/auth-guest/pkg/service/authguest/options.go similarity index 63% rename from services/guestauth/pkg/service/guestauth/options.go rename to services/auth-guest/pkg/service/authguest/options.go index ca17225a2f..b67b8f585e 100644 --- a/services/guestauth/pkg/service/guestauth/options.go +++ b/services/auth-guest/pkg/service/authguest/options.go @@ -1,15 +1,18 @@ -package guestauth +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + +package authguest import ( gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/jwt" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/jwt" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" ) type Option func(*Options) -// Options for the guestauth service +// Options for the auth-guest service type Options struct { GatewaySelector pool.Selectable[gateway.GatewayAPIClient] ServiceAccount config.ServiceAccount @@ -23,14 +26,14 @@ func GatewaySelector(gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) } } -// ServiceAccount configures a service account for the guestauth service +// ServiceAccount configures a service account for the auth-guest service func ServiceAccount(sa config.ServiceAccount) Option { return func(o *Options) { o.ServiceAccount = sa } } -// JWT configures the jwt service for the guestauth service +// JWT configures the jwt service for the auth-guest service func JWT(m *jwt.JwtService) Option { return func(o *Options) { o.JWT = m diff --git a/services/guestauth/pkg/service/guestauth/service.go b/services/auth-guest/pkg/service/authguest/service.go similarity index 79% rename from services/guestauth/pkg/service/guestauth/service.go rename to services/auth-guest/pkg/service/authguest/service.go index 710b7cca49..5752d8de34 100644 --- a/services/guestauth/pkg/service/guestauth/service.go +++ b/services/auth-guest/pkg/service/authguest/service.go @@ -1,4 +1,7 @@ -package guestauth +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + +package authguest import ( "context" @@ -10,10 +13,10 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/jwt" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/jwt" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/storage" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/token" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" "github.com/opencloud-eu/reva/v2/pkg/utils" ) @@ -34,17 +37,17 @@ type RedeemError struct { func (e *RedeemError) Error() string { return e.ErrorType.Error() } -// GuestAuth is the domain service used by the transport and event layers. -type GuestAuth interface { +// AuthGuest is the domain service used by the transport and event layers. +type AuthGuest interface { CreateToken(ctx context.Context, shareID string) (*token.Token, error) Redeem(ctx context.Context, tokenString string) (string, error) CleanupShare(shareID string) error } -var _ GuestAuth = (*GuestAuthService)(nil) +var _ AuthGuest = (*AuthGuestService)(nil) -// GuestAuthService contains the business logic shared by guestauth transport services. -type GuestAuthService struct { +// AuthGuestService contains the business logic shared by auth-guest transport services. +type AuthGuestService struct { tokenSvc *token.TokenService store storage.Manager gatewaySelector pool.Selectable[gateway.GatewayAPIClient] @@ -52,13 +55,13 @@ type GuestAuthService struct { jwtService *jwt.JwtService } -func NewGuestAuthService(tokenSvc *token.TokenService, store storage.Manager, opts ...Option) *GuestAuthService { +func NewAuthGuestService(tokenSvc *token.TokenService, store storage.Manager, opts ...Option) *AuthGuestService { o := &Options{} for _, opt := range opts { opt(o) } - return &GuestAuthService{ + return &AuthGuestService{ tokenSvc: tokenSvc, store: store, gatewaySelector: o.GatewaySelector, @@ -67,7 +70,7 @@ func NewGuestAuthService(tokenSvc *token.TokenService, store storage.Manager, op } } -func (s *GuestAuthService) CreateToken(ctx context.Context, shareID string) (*token.Token, error) { +func (s *AuthGuestService) CreateToken(ctx context.Context, shareID string) (*token.Token, error) { tok, err := s.tokenSvc.Generate(shareID) if err != nil { return nil, err @@ -87,7 +90,7 @@ func (s *GuestAuthService) CreateToken(ctx context.Context, shareID string) (*to } // Redeem validates a token and its share and exchanges them for a session token. -func (s *GuestAuthService) Redeem(ctx context.Context, tokenString string) (string, error) { +func (s *AuthGuestService) Redeem(ctx context.Context, tokenString string) (string, error) { rec, err := s.verifyToken(tokenString) if err != nil { return "", err @@ -108,7 +111,7 @@ func (s *GuestAuthService) Redeem(ctx context.Context, tokenString string) (stri } // CleanupShare removes a share's token record from storage. Missing records are ignored. -func (s *GuestAuthService) CleanupShare(shareID string) error { +func (s *AuthGuestService) CleanupShare(shareID string) error { shareIDHash := token.Hash(shareID) err := s.store.Remove(shareIDHash) if err != nil && err != storage.ErrNotFound { @@ -119,7 +122,7 @@ func (s *GuestAuthService) CleanupShare(shareID string) error { } // VerifyToken validates a token and returns its stored record. -func (s *GuestAuthService) verifyToken(tokenString string) (*storage.Record, error) { +func (s *AuthGuestService) verifyToken(tokenString string) (*storage.Record, error) { tok, err := s.tokenSvc.Parse(tokenString) if err != nil { return nil, &RedeemError{ErrorType: err} @@ -146,7 +149,7 @@ func (s *GuestAuthService) verifyToken(tokenString string) (*storage.Record, err } // validateShare extracts the share information from the gateway and checks its existence and expiration. -func (s *GuestAuthService) validateShare(ctx context.Context, shareID string) (*collaboration.Share, error) { +func (s *AuthGuestService) validateShare(ctx context.Context, shareID string) (*collaboration.Share, error) { share, err := s.getShare(ctx, shareID) if err != nil { return nil, &RedeemError{ErrorType: err, ShareID: shareID} @@ -160,7 +163,7 @@ func (s *GuestAuthService) validateShare(ctx context.Context, shareID string) (* } // getShare fetches a share from the gateway. -func (s *GuestAuthService) getShare(ctx context.Context, shareID string) (*collaboration.Share, error) { +func (s *AuthGuestService) getShare(ctx context.Context, shareID string) (*collaboration.Share, error) { gwc, err := s.gatewaySelector.Next() if err != nil { return nil, err diff --git a/services/guestauth/pkg/service/guestauth/service_test.go b/services/auth-guest/pkg/service/authguest/service_test.go similarity index 90% rename from services/guestauth/pkg/service/guestauth/service_test.go rename to services/auth-guest/pkg/service/authguest/service_test.go index 38253a3f52..18595a926c 100644 --- a/services/guestauth/pkg/service/guestauth/service_test.go +++ b/services/auth-guest/pkg/service/authguest/service_test.go @@ -1,4 +1,7 @@ -package guestauth +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + +package authguest import ( "context" @@ -8,11 +11,11 @@ import ( gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/config" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/jwt" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" - storagemocks "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage/mocks" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/config" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/jwt" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/storage" + storagemocks "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/storage/mocks" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/token" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" "github.com/opencloud-eu/reva/v2/pkg/utils" cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" @@ -61,9 +64,9 @@ func newToken(t *testing.T) (string, storage.Record) { return tok.String(), rec } -func newShareService(t *testing.T, gwc *cs3mocks.GatewayAPIClient) *GuestAuthService { +func newShareService(t *testing.T, gwc *cs3mocks.GatewayAPIClient) *AuthGuestService { t.Helper() - return NewGuestAuthService( + return NewAuthGuestService( token.NewTokenService(), storagemocks.NewManager(t), GatewaySelector(newGatewayTestSelector(gwc)), @@ -71,9 +74,9 @@ func newShareService(t *testing.T, gwc *cs3mocks.GatewayAPIClient) *GuestAuthSer ) } -func newRedeemService(t *testing.T, store storage.Manager, gwc *cs3mocks.GatewayAPIClient) *GuestAuthService { +func newRedeemService(t *testing.T, store storage.Manager, gwc *cs3mocks.GatewayAPIClient) *AuthGuestService { t.Helper() - return NewGuestAuthService( + return NewAuthGuestService( token.NewTokenService(), store, GatewaySelector(newGatewayTestSelector(gwc)), @@ -98,7 +101,7 @@ func TestCreateTokenPersistsRecord(t *testing.T) { added = args.Get(0).(storage.Record) }).Return(nil) - s := NewGuestAuthService( + s := NewAuthGuestService( token.NewTokenService(), store, GatewaySelector(newGatewayTestSelector(gwc)), @@ -131,7 +134,7 @@ func TestVerifyToken(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { store := storagemocks.NewManager(t) - s := NewGuestAuthService(token.NewTokenService(), store) + s := NewAuthGuestService(token.NewTokenService(), store) tok, rec := newToken(t) if tt.expired { rec.Expiry = time.Now().Add(-time.Hour) diff --git a/services/guestauth/pkg/service/events/handlers.go b/services/auth-guest/pkg/service/events/handlers.go similarity index 84% rename from services/guestauth/pkg/service/events/handlers.go rename to services/auth-guest/pkg/service/events/handlers.go index ca41413da8..cabd003703 100644 --- a/services/guestauth/pkg/service/events/handlers.go +++ b/services/auth-guest/pkg/service/events/handlers.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package events import ( @@ -19,7 +22,7 @@ func (s *EventConsumer) handleShareCreated(ctx context.Context, ev events.ShareC return nil } - tok, err := s.guestAuth.CreateToken(ctx, ev.ShareID.GetOpaqueId()) + tok, err := s.authGuest.CreateToken(ctx, ev.ShareID.GetOpaqueId()) if err != nil { return err } @@ -42,7 +45,7 @@ func (s *EventConsumer) handleShareRemoved(ctx context.Context, ev events.ShareR s.log.Debug().Interface("event", ev).Msg("share removed event received") - return s.guestAuth.CleanupShare(ev.ShareID.GetOpaqueId()) + return s.authGuest.CleanupShare(ev.ShareID.GetOpaqueId()) } // handleShareExpired handles a share expired event. @@ -52,5 +55,5 @@ func (s *EventConsumer) handleShareExpired(ctx context.Context, ev events.ShareE s.log.Debug().Interface("event", ev).Msg("share expired event received") - return s.guestAuth.CleanupShare(ev.ShareID.GetOpaqueId()) + return s.authGuest.CleanupShare(ev.ShareID.GetOpaqueId()) } diff --git a/services/guestauth/pkg/service/events/handlers_test.go b/services/auth-guest/pkg/service/events/handlers_test.go similarity index 84% rename from services/guestauth/pkg/service/events/handlers_test.go rename to services/auth-guest/pkg/service/events/handlers_test.go index cfea2ede84..8c98f0df16 100644 --- a/services/guestauth/pkg/service/events/handlers_test.go +++ b/services/auth-guest/pkg/service/events/handlers_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package events import ( @@ -8,9 +11,9 @@ import ( collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" ocEvents "github.com/opencloud-eu/opencloud/pkg/events" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth/mocks" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest/mocks" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/token" "github.com/opencloud-eu/reva/v2/pkg/events" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -33,10 +36,10 @@ func (tb *testBus) Consume(_ string, _ ...microevents.ConsumeOption) (<-chan mic return nil, nil } -func newConsumer(t *testing.T, guestAuth guestauth.GuestAuth) (*EventConsumer, *testBus) { +func newConsumer(t *testing.T, authGuest authguest.AuthGuest) (*EventConsumer, *testBus) { t.Helper() bus := &testBus{} - consumer, err := NewEventConsumer(bus, GuestAuthService(guestAuth)) + consumer, err := NewEventConsumer(bus, AuthGuestService(authGuest)) require.NoError(t, err) return consumer, bus @@ -46,7 +49,7 @@ func TestHandleShareCreated(t *testing.T) { tok, err := token.NewTokenService().Generate(testShareID) require.NoError(t, err) - svcMock := mocks.NewGuestAuth(t) + svcMock := mocks.NewAuthGuest(t) svcMock.On("CreateToken", mock.Anything, testShareID).Return(tok, nil) svc, bus := newConsumer(t, svcMock) @@ -75,7 +78,7 @@ func TestHandleShareCreated(t *testing.T) { } func TestHandleShareCreatedSkipsNonGuest(t *testing.T) { - svcMock := mocks.NewGuestAuth(t) + svcMock := mocks.NewAuthGuest(t) svc, bus := newConsumer(t, svcMock) ev := events.ShareCreated{ @@ -90,7 +93,7 @@ func TestHandleShareCreatedSkipsNonGuest(t *testing.T) { } func TestHandleShareRemoved(t *testing.T) { - svcMock := mocks.NewGuestAuth(t) + svcMock := mocks.NewAuthGuest(t) svcMock.On("CleanupShare", testShareID).Return(nil) svc, _ := newConsumer(t, svcMock) @@ -104,7 +107,7 @@ func TestHandleShareRemoved(t *testing.T) { } func TestHandleShareExpired(t *testing.T) { - svcMock := mocks.NewGuestAuth(t) + svcMock := mocks.NewAuthGuest(t) svcMock.On("CleanupShare", testShareID).Return(nil) svc, _ := newConsumer(t, svcMock) diff --git a/services/guestauth/pkg/service/events/options.go b/services/auth-guest/pkg/service/events/options.go similarity index 61% rename from services/guestauth/pkg/service/events/options.go rename to services/auth-guest/pkg/service/events/options.go index a3f8e10d96..d9c7c0eacb 100644 --- a/services/guestauth/pkg/service/events/options.go +++ b/services/auth-guest/pkg/service/events/options.go @@ -1,41 +1,44 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package events import ( "context" "github.com/opencloud-eu/opencloud/pkg/log" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest" "github.com/opencloud-eu/reva/v2/pkg/events" ) -// Option for the guestauth service +// Option for the auth-guest service type Option func(*Options) -// Options for the guestauth service +// Options for the auth-guest service type Options struct { Context context.Context Logger log.Logger Stream events.Stream RegisteredEvents []events.Unmarshaller NumConsumers int - GuestAuthService guestauth.GuestAuth + AuthGuestService authguest.AuthGuest } -// Context configures a context for the guestauth service +// Context configures a context for the auth-guest service func Context(ctx context.Context) Option { return func(o *Options) { o.Context = ctx } } -// Logger configures a logger for the guestauth service +// Logger configures a logger for the auth-guest service func Logger(log log.Logger) Option { return func(o *Options) { o.Logger = log } } -// Stream configures an event stream for the guestauth service +// Stream configures an event stream for the auth-guest service func Stream(s events.Stream) Option { return func(o *Options) { o.Stream = s @@ -56,9 +59,9 @@ func NumConsumers(num int) Option { } } -// GuestAuthService configures the guest auth domain service. -func GuestAuthService(s guestauth.GuestAuth) Option { +// AuthGuestService configures the guest auth domain service. +func AuthGuestService(s authguest.AuthGuest) Option { return func(o *Options) { - o.GuestAuthService = s + o.AuthGuestService = s } } diff --git a/services/guestauth/pkg/service/events/service.go b/services/auth-guest/pkg/service/events/service.go similarity index 87% rename from services/guestauth/pkg/service/events/service.go rename to services/auth-guest/pkg/service/events/service.go index 01304769e8..6e346febf3 100644 --- a/services/guestauth/pkg/service/events/service.go +++ b/services/auth-guest/pkg/service/events/service.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package events import ( @@ -6,7 +9,7 @@ import ( "sync/atomic" "github.com/opencloud-eu/opencloud/pkg/log" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/guestauth" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest" "github.com/opencloud-eu/reva/v2/pkg/events" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -15,7 +18,7 @@ import ( var tracer trace.Tracer func init() { - tracer = otel.Tracer("github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/events") + tracer = otel.Tracer("github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/events") } var ( @@ -28,7 +31,7 @@ type EventConsumer struct { log log.Logger stream events.Stream - guestAuth guestauth.GuestAuth + authGuest authguest.AuthGuest numConsumers int @@ -51,7 +54,7 @@ func NewEventConsumer(stream events.Stream, opts ...Option) (*EventConsumer, err ctx: o.Context, log: o.Logger, stream: stream, - guestAuth: o.GuestAuthService, + authGuest: o.AuthGuestService, events: o.RegisteredEvents, numConsumers: o.NumConsumers, stopCh: make(chan struct{}, 1), @@ -63,7 +66,7 @@ func NewEventConsumer(stream events.Stream, opts ...Option) (*EventConsumer, err // Run to fulfil Runner interface func (s *EventConsumer) Run() error { - ch, err := events.Consume(s.stream, "guestauth", s.events...) + ch, err := events.Consume(s.stream, "auth-guest", s.events...) if err != nil { return err } @@ -73,7 +76,7 @@ func (s *EventConsumer) Run() error { defer cancel() s.log.Debug().Int("worker.count", s.numConsumers). - Str("messaging.consumer.group.name", "guestauth"). + Str("messaging.consumer.group.name", "auth-guest"). Str("messaging.system", "nats"). Str("messaging.operation.name", "receive"). Msg("starting event processing workers") diff --git a/services/guestauth/pkg/service/jwt/jwt.go b/services/auth-guest/pkg/service/jwt/jwt.go similarity index 88% rename from services/guestauth/pkg/service/jwt/jwt.go rename to services/auth-guest/pkg/service/jwt/jwt.go index 3721f7d697..1ba9e95f9e 100644 --- a/services/guestauth/pkg/service/jwt/jwt.go +++ b/services/auth-guest/pkg/service/jwt/jwt.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package jwt import ( diff --git a/services/guestauth/pkg/service/jwt/jwt_test.go b/services/auth-guest/pkg/service/jwt/jwt_test.go similarity index 91% rename from services/guestauth/pkg/service/jwt/jwt_test.go rename to services/auth-guest/pkg/service/jwt/jwt_test.go index f2952b7959..2ec12302c2 100644 --- a/services/guestauth/pkg/service/jwt/jwt_test.go +++ b/services/auth-guest/pkg/service/jwt/jwt_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package jwt import ( diff --git a/services/guestauth/pkg/service/storage/file_manager.go b/services/auth-guest/pkg/service/storage/file_manager.go similarity index 97% rename from services/guestauth/pkg/service/storage/file_manager.go rename to services/auth-guest/pkg/service/storage/file_manager.go index 241a30ad62..828d39095b 100644 --- a/services/guestauth/pkg/service/storage/file_manager.go +++ b/services/auth-guest/pkg/service/storage/file_manager.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package storage import ( diff --git a/services/guestauth/pkg/service/storage/file_manager_test.go b/services/auth-guest/pkg/service/storage/file_manager_test.go similarity index 95% rename from services/guestauth/pkg/service/storage/file_manager_test.go rename to services/auth-guest/pkg/service/storage/file_manager_test.go index ebf2bc3df8..737f505000 100644 --- a/services/guestauth/pkg/service/storage/file_manager_test.go +++ b/services/auth-guest/pkg/service/storage/file_manager_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package storage import ( @@ -7,7 +10,7 @@ import ( "testing" "time" - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/token" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/token" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/services/guestauth/pkg/service/storage/mocks/manager.go b/services/auth-guest/pkg/service/storage/mocks/manager.go similarity index 98% rename from services/guestauth/pkg/service/storage/mocks/manager.go rename to services/auth-guest/pkg/service/storage/mocks/manager.go index 3f49760c3a..674c553c58 100644 --- a/services/guestauth/pkg/service/storage/mocks/manager.go +++ b/services/auth-guest/pkg/service/storage/mocks/manager.go @@ -5,7 +5,7 @@ package mocks import ( - "github.com/opencloud-eu/opencloud/services/guestauth/pkg/service/storage" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/storage" mock "github.com/stretchr/testify/mock" ) diff --git a/services/guestauth/pkg/service/storage/storage.go b/services/auth-guest/pkg/service/storage/storage.go similarity index 87% rename from services/guestauth/pkg/service/storage/storage.go rename to services/auth-guest/pkg/service/storage/storage.go index eec6ab6cca..b9c83bd72e 100644 --- a/services/guestauth/pkg/service/storage/storage.go +++ b/services/auth-guest/pkg/service/storage/storage.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package storage import ( diff --git a/services/guestauth/pkg/service/token/token.go b/services/auth-guest/pkg/service/token/token.go similarity index 94% rename from services/guestauth/pkg/service/token/token.go rename to services/auth-guest/pkg/service/token/token.go index c4c7214c9f..e997845a62 100644 --- a/services/guestauth/pkg/service/token/token.go +++ b/services/auth-guest/pkg/service/token/token.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package token import ( diff --git a/services/guestauth/pkg/service/token/token_test.go b/services/auth-guest/pkg/service/token/token_test.go similarity index 96% rename from services/guestauth/pkg/service/token/token_test.go rename to services/auth-guest/pkg/service/token/token_test.go index 1fc66285b3..f80fbfd882 100644 --- a/services/guestauth/pkg/service/token/token_test.go +++ b/services/auth-guest/pkg/service/token/token_test.go @@ -1,3 +1,6 @@ +// Copyright 2026 OpenCloud GmbH +// SPDX-License-Identifier: Apache-2.0 + package token import ( diff --git a/services/guestauth/pkg/config/config.go b/services/guestauth/pkg/config/config.go deleted file mode 100644 index ae795fef94..0000000000 --- a/services/guestauth/pkg/config/config.go +++ /dev/null @@ -1,87 +0,0 @@ -package config - -import ( - "context" - "time" - - "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:"-"` - - LogLevel string `yaml:"loglevel" env:"OC_LOG_LEVEL;GUESTAUTH_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"%%NEXT%%"` - - Debug Debug `yaml:"debug"` - - Events Events `yaml:"events"` - - RevaGateway string `yaml:"reva_gateway" env:"OC_REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata" introductionVersion:"%%NEXT%%"` - GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"` - - HTTP HTTP `yaml:"http"` - Storage Storage `yaml:"storage"` - TokenManager *TokenManager `yaml:"token_manager"` - JWT JWT `yaml:"jwt"` - - ServiceAccount ServiceAccount `yaml:"service_account"` - - NumConsumers int `yaml:"num_consumers" env:"GUESTAUTH_NUM_CONSUMERS" desc:"The amount of concurrent event consumers to start. Event consumers are used for processing events. Multiple consumers increase parallelisation, but will also increase CPU and memory demands." introductionVersion:"%%NEXT%%"` - - Context context.Context `yaml:"-"` -} - -// Events combines the configuration options for the event bus. -type Events struct { - Disabled bool `yaml:"disabled" env:"GUESTAUTH_EVENTS_DISABLED" desc:"Disables listening for events. Set this to true if the service should only handle HTTP requests." introductionVersion:"%%NEXT%%"` - Endpoint string `yaml:"endpoint" env:"OC_EVENTS_ENDPOINT" desc:"The address of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture." introductionVersion:"%%NEXT%%"` - Cluster string `yaml:"cluster" env:"OC_EVENTS_CLUSTER" desc:"The clusterID of the event system. The event system is the message queuing service. It is used as message broker for the microservice architecture. Mandatory when using NATS as event system." introductionVersion:"%%NEXT%%"` - TLSInsecure bool `yaml:"tls_insecure" env:"OC_INSECURE;OC_EVENTS_TLS_INSECURE" desc:"Whether to verify the server TLS certificates." introductionVersion:"%%NEXT%%"` - TLSRootCACertificate string `yaml:"tls_root_ca_certificate" env:"OC_EVENTS_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided GUESTAUTH_EVENTS_TLS_INSECURE will be seen as false." introductionVersion:"%%NEXT%%"` - EnableTLS bool `yaml:"enable_tls" env:"OC_EVENTS_ENABLE_TLS" desc:"Enable TLS for the connection to the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"%%NEXT%%"` - AuthUsername string `yaml:"username" env:"OC_EVENTS_AUTH_USERNAME" desc:"The username to authenticate with the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"%%NEXT%%"` - AuthPassword string `yaml:"password" env:"OC_EVENTS_AUTH_PASSWORD" desc:"The password to authenticate with the events broker. The events broker is the OpenCloud service which receives and delivers events between the services." introductionVersion:"%%NEXT%%"` -} - -// ServiceAccount is the configuration for the used service account -type ServiceAccount struct { - ServiceAccountID string `yaml:"service_account_id" env:"OC_SERVICE_ACCOUNT_ID;GUESTAUTH_SERVICE_ACCOUNT_ID" desc:"The ID of the service account the service should use. See the 'auth-service' service description for more details." introductionVersion:"%%NEXT%%"` - ServiceAccountSecret string `yaml:"service_account_secret" env:"OC_SERVICE_ACCOUNT_SECRET;GUESTAUTH_SERVICE_ACCOUNT_SECRET" desc:"The service account secret." introductionVersion:"%%NEXT%%"` -} - -// CORS defines the available cors configuration. -type CORS struct { - AllowedOrigins []string `yaml:"allow_origins" env:"OC_CORS_ALLOW_ORIGINS;GUESTAUTH_CORS_ALLOW_ORIGINS" desc:"A 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. See the Environment Variable Types description for more details." introductionVersion:"%%NEXT%%"` - AllowedMethods []string `yaml:"allow_methods" env:"OC_CORS_ALLOW_METHODS;GUESTAUTH_CORS_ALLOW_METHODS" desc:"A 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. See the Environment Variable Types description for more details." introductionVersion:"%%NEXT%%"` - AllowedHeaders []string `yaml:"allow_headers" env:"OC_CORS_ALLOW_HEADERS;GUESTAUTH_CORS_ALLOW_HEADERS" desc:"A 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. See the Environment Variable Types description for more details." introductionVersion:"%%NEXT%%"` - AllowCredentials bool `yaml:"allow_credentials" env:"OC_CORS_ALLOW_CREDENTIALS;GUESTAUTH_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." introductionVersion:"%%NEXT%%"` -} - -// HTTP defines the available http configuration. -type HTTP struct { - Disabled bool `yaml:"disabled" env:"GUESTAUTH_HTTP_DISABLED" desc:"Disables the HTTP service. Set this to true if the service should only handle events." introductionVersion:"%%NEXT%%"` - Addr string `yaml:"addr" env:"GUESTAUTH_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"%%NEXT%%"` - Namespace string `yaml:"-"` - Root string `yaml:"root" env:"GUESTAUTH_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"%%NEXT%%"` - CORS CORS `yaml:"cors"` - TLS shared.HTTPServiceTLS `yaml:"tls"` -} - -// Storage defines the configuration for the token storage. -type Storage struct { - RootDirectory string `yaml:"root_directory" env:"GUESTAUTH_TOKENS_STORAGE_ROOT" desc:"The directory where the guest share tokens are stored. If not defined, the root directory derives from $OC_BASE_DATA_PATH/guestauth." introductionVersion:"%%NEXT%%"` -} - -// TokenManager is the config for using the reva token manager -type TokenManager struct { - JWTSecret string `yaml:"jwt_secret" env:"GUESTAUTH_JWT_SECRET" desc:"The secret to mint and validate jwt tokens." introductionVersion:"%%NEXT%%"` -} - -// JWT defines the configuration for guest session tokens. -type JWT struct { - CookieName string `yaml:"cookie_name" env:"GUESTAUTH_JWT_COOKIE_NAME" desc:"The name of the session cookie set when a guest token is redeemed." introductionVersion:"%%NEXT%%"` - TTL time.Duration `yaml:"ttl" env:"GUESTAUTH_JWT_TTL" desc:"The lifetime of a redeemed guest session token." introductionVersion:"%%NEXT%%"` -} diff --git a/services/guestauth/pkg/config/debug.go b/services/guestauth/pkg/config/debug.go deleted file mode 100644 index 4912b1825c..0000000000 --- a/services/guestauth/pkg/config/debug.go +++ /dev/null @@ -1,9 +0,0 @@ -package config - -// Debug defines the available debug configuration. -type Debug struct { - Addr string `yaml:"addr" env:"GUESTAUTH_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"%%NEXT%%"` - Token string `yaml:"token" env:"GUESTAUTH_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"%%NEXT%%"` - Pprof bool `yaml:"pprof" env:"GUESTAUTH_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"%%NEXT%%"` - Zpages bool `yaml:"zpages" env:"GUESTAUTH_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"%%NEXT%%"` -} diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index 51ca7b373c..68d9273ccc 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -285,7 +285,7 @@ func DefaultPolicies() []config.Policy { }, { Endpoint: "/graph/v1beta1/extensions/org.libregraph/guestInvitations", - Service: "eu.opencloud.web.guestauth", + Service: "eu.opencloud.web.auth-guest", Unprotected: true, }, { From d0847093bf760a5108209eaf41976d18f8197478 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Tue, 29 Sep 2026 16:21:27 +0200 Subject: [PATCH 25/32] feat(guestauth): prefix for cookie --- services/auth-guest/pkg/config/defaults/defaultconfig.go | 2 +- services/auth-guest/pkg/server/http/redeem_test.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/services/auth-guest/pkg/config/defaults/defaultconfig.go b/services/auth-guest/pkg/config/defaults/defaultconfig.go index 517047db04..52e2632c6e 100644 --- a/services/auth-guest/pkg/config/defaults/defaultconfig.go +++ b/services/auth-guest/pkg/config/defaults/defaultconfig.go @@ -55,7 +55,7 @@ func DefaultConfig() *config.Config { RootDirectory: path.Join(defaults.BaseDataPath(), "auth-guest"), }, JWT: config.JWT{ - CookieName: "oc_guest_session", + CookieName: "__Host-oc_guest_session", TTL: 24 * time.Hour, }, } diff --git a/services/auth-guest/pkg/server/http/redeem_test.go b/services/auth-guest/pkg/server/http/redeem_test.go index c5579a0519..f72c66ae3a 100644 --- a/services/auth-guest/pkg/server/http/redeem_test.go +++ b/services/auth-guest/pkg/server/http/redeem_test.go @@ -26,7 +26,7 @@ func newRedeemHandler(t *testing.T, svc authguest.AuthGuest) http.HandlerFunc { t.Helper() cfg := &config.Config{ JWT: config.JWT{ - CookieName: "oc_guest_session", + CookieName: "__Host-oc_guest_session", TTL: time.Hour, }, } @@ -47,13 +47,14 @@ func TestRedeemHandler(t *testing.T) { var cookie *http.Cookie for _, c := range rr.Result().Cookies() { - if c.Name == "oc_guest_session" { + if c.Name == "__Host-oc_guest_session" { cookie = c } } require.NotNil(t, cookie) assert.Equal(t, "session-token", cookie.Value) assert.True(t, cookie.HttpOnly) + assert.True(t, cookie.Secure) assert.Equal(t, "/", cookie.Path) } From e17a9a3ae988edf5b50b8713150b53d078e742f1 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Wed, 30 Sep 2026 09:14:50 +0200 Subject: [PATCH 26/32] feat(guestauth): aligned readme --- services/auth-guest/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/auth-guest/README.md b/services/auth-guest/README.md index 167cc41fa4..43b0bcdc51 100644 --- a/services/auth-guest/README.md +++ b/services/auth-guest/README.md @@ -27,9 +27,9 @@ It is part of the default service set and does not need to be enabled with the hash of the share id. It then publishes the `GuestTokenCreated` event with the token. 2. **Redeem** — the guest posts the token to - `POST /graph/v1beta1/extensions/org.libregraph/guestInvitations/redeem`. The service validates the - token and the share, marks the token as used and returns a signed JWT - session token in a cookie. Tokens are single-use. + `POST /graph/v1beta1/extensions/org.libregraph/guestInvitations/redeem`. + The service validates the token and the share, marks the token as used + and returns a signed JWT session token in a cookie. Tokens are single-use. 3. **Cleanup** — on the consumed `ShareRemoved` or `ShareExpired` event, the stored record is deleted. From 340851de704d8b79acc26d4038b7e82792fcb0c7 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Wed, 30 Sep 2026 11:52:05 +0200 Subject: [PATCH 27/32] feat(guestauth): updated redeem endpoint response and dependencies --- services/auth-guest/README.md | 3 +- services/auth-guest/pkg/server/http/errors.go | 28 ++++++------- services/auth-guest/pkg/server/http/redeem.go | 12 ++++-- .../auth-guest/pkg/server/http/redeem_test.go | 42 ++++++++++--------- .../pkg/service/authguest/mocks/auth_guest.go | 19 +++++---- .../pkg/service/authguest/service.go | 28 +++++++++---- .../pkg/service/authguest/service_test.go | 5 ++- services/auth-guest/pkg/service/jwt/jwt.go | 2 +- 8 files changed, 83 insertions(+), 56 deletions(-) diff --git a/services/auth-guest/README.md b/services/auth-guest/README.md index 43b0bcdc51..5956dadc97 100644 --- a/services/auth-guest/README.md +++ b/services/auth-guest/README.md @@ -29,7 +29,8 @@ It is part of the default service set and does not need to be enabled with 2. **Redeem** — the guest posts the token to `POST /graph/v1beta1/extensions/org.libregraph/guestInvitations/redeem`. The service validates the token and the share, marks the token as used - and returns a signed JWT session token in a cookie. Tokens are single-use. + and returns a signed JWT session token in a cookie plus the share's + `permissionId` in the response body. Tokens are single-use. 3. **Cleanup** — on the consumed `ShareRemoved` or `ShareExpired` event, the stored record is deleted. diff --git a/services/auth-guest/pkg/server/http/errors.go b/services/auth-guest/pkg/server/http/errors.go index 95834fac62..869d0f6234 100644 --- a/services/auth-guest/pkg/server/http/errors.go +++ b/services/auth-guest/pkg/server/http/errors.go @@ -14,9 +14,9 @@ import ( ) type errorResponse struct { - ErrorType string `json:"error_type"` - Message string `json:"message"` - ShareID string `json:"share_id"` + ErrorType string `json:"errorType"` + Message string `json:"message"` + PermissionID string `json:"permissionId"` } func writeError(w http.ResponseWriter, status int, body errorResponse) { @@ -28,33 +28,33 @@ func writeError(w http.ResponseWriter, status int, body errorResponse) { func writeRedeemError(w http.ResponseWriter, err error) { var re *authguest.RedeemError if !errors.As(err, &re) { - writeError(w, http.StatusInternalServerError, errorResponse{ErrorType: "internal_error", Message: "An internal error occurred."}) + writeError(w, http.StatusInternalServerError, errorResponse{ErrorType: "internalError", Message: "An internal error occurred."}) return } status := http.StatusInternalServerError - errorType := "internal_error" + errorType := "internalError" switch { case errors.Is(re.ErrorType, authguest.ErrExpired): - status, errorType = http.StatusUnauthorized, "token_expired" + status, errorType = http.StatusUnauthorized, "tokenExpired" case errors.Is(re.ErrorType, token.ErrInvalidToken): - status, errorType = http.StatusUnauthorized, "token_invalid" + status, errorType = http.StatusUnauthorized, "tokenInvalid" case errors.Is(re.ErrorType, storage.ErrNotFound): - status, errorType = http.StatusNotFound, "token_not_found" + status, errorType = http.StatusNotFound, "tokenNotFound" case errors.Is(re.ErrorType, storage.ErrInvalidHash): - status, errorType = http.StatusUnauthorized, "token_invalid" + status, errorType = http.StatusUnauthorized, "tokenInvalid" case errors.Is(re.ErrorType, authguest.ErrAlreadyRedeemed): - status, errorType = http.StatusConflict, "token_already_redeemed" + status, errorType = http.StatusConflict, "tokenAlreadyRedeemed" case errors.Is(re.ErrorType, authguest.ErrShareNotFound): - status, errorType = http.StatusNotFound, "share_not_found" + status, errorType = http.StatusNotFound, "shareNotFound" case errors.Is(re.ErrorType, authguest.ErrShareExpired): - status, errorType = http.StatusGone, "share_expired" + status, errorType = http.StatusGone, "shareExpired" } message := re.ErrorType.Error() - if errorType == "internal_error" { + if errorType == "internalError" { message = "An internal error occurred." } - writeError(w, status, errorResponse{ErrorType: errorType, Message: message, ShareID: re.ShareID}) + writeError(w, status, errorResponse{ErrorType: errorType, Message: message, PermissionID: re.ShareID}) } diff --git a/services/auth-guest/pkg/server/http/redeem.go b/services/auth-guest/pkg/server/http/redeem.go index 3102a89f07..09bd63c5d0 100644 --- a/services/auth-guest/pkg/server/http/redeem.go +++ b/services/auth-guest/pkg/server/http/redeem.go @@ -17,17 +17,21 @@ type RedeemRequest struct { Token string `json:"token"` } +type redeemResponse struct { + PermissionID string `json:"permissionId"` +} + // RedeemHandler validates the token submitted to the redeem endpoint. func RedeemHandler(log log.Logger, s authguest.AuthGuest, cfg *config.Config) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var req RedeemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { log.Debug().Err(err).Msg("request body is malformed") - writeError(w, http.StatusBadRequest, errorResponse{ErrorType: "invalid_request", Message: "The request body is malformed."}) + writeError(w, http.StatusBadRequest, errorResponse{ErrorType: "invalidRequest", Message: "The request body is malformed."}) return } - sessionToken, err := s.Redeem(r.Context(), req.Token) + result, err := s.Redeem(r.Context(), req.Token) if err != nil { log.Debug().Err(err).Msg("redeem failed") writeRedeemError(w, err) @@ -36,13 +40,15 @@ func RedeemHandler(log log.Logger, s authguest.AuthGuest, cfg *config.Config) fu http.SetCookie(w, &http.Cookie{ Name: cfg.JWT.CookieName, - Value: sessionToken, + Value: result.SessionToken, Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode, MaxAge: int(cfg.JWT.TTL.Seconds()), }) + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(redeemResponse{PermissionID: result.ShareID}) } } diff --git a/services/auth-guest/pkg/server/http/redeem_test.go b/services/auth-guest/pkg/server/http/redeem_test.go index f72c66ae3a..28a9320601 100644 --- a/services/auth-guest/pkg/server/http/redeem_test.go +++ b/services/auth-guest/pkg/server/http/redeem_test.go @@ -35,7 +35,7 @@ func newRedeemHandler(t *testing.T, svc authguest.AuthGuest) http.HandlerFunc { func TestRedeemHandler(t *testing.T) { svcMock := mocks.NewAuthGuest(t) - svcMock.On("Redeem", mock.Anything, "valid-token").Return("session-token", nil) + svcMock.On("Redeem", mock.Anything, "valid-token").Return(&authguest.RedeemResponse{SessionToken: "session-token", ShareID: "share-1"}, nil) body, err := json.Marshal(RedeemRequest{Token: "valid-token"}) require.NoError(t, err) @@ -56,59 +56,63 @@ func TestRedeemHandler(t *testing.T) { assert.True(t, cookie.HttpOnly) assert.True(t, cookie.Secure) assert.Equal(t, "/", cookie.Path) + + var resp redeemResponse + require.NoError(t, json.NewDecoder(rr.Body).Decode(&resp)) + assert.Equal(t, "share-1", resp.PermissionID) } func TestRedeemHandlerErrorMapping(t *testing.T) { tests := []struct { - name string - err error - wantStatus int - wantType string - wantShare string + name string + err error + wantStatus int + wantType string + wantPermission string }{ { - name: "token expired", - err: &authguest.RedeemError{ErrorType: authguest.ErrExpired, ShareID: "share-1"}, - wantStatus: http.StatusUnauthorized, - wantType: "token_expired", - wantShare: "share-1", + name: "token expired", + err: &authguest.RedeemError{ErrorType: authguest.ErrExpired, ShareID: "share-1"}, + wantStatus: http.StatusUnauthorized, + wantType: "tokenExpired", + wantPermission: "share-1", }, { name: "token invalid", err: &authguest.RedeemError{ErrorType: token.ErrInvalidToken}, wantStatus: http.StatusUnauthorized, - wantType: "token_invalid", + wantType: "tokenInvalid", }, { name: "token not found", err: &authguest.RedeemError{ErrorType: storage.ErrNotFound}, wantStatus: http.StatusNotFound, - wantType: "token_not_found", + wantType: "tokenNotFound", }, { name: "token already redeemed", err: &authguest.RedeemError{ErrorType: authguest.ErrAlreadyRedeemed}, wantStatus: http.StatusConflict, - wantType: "token_already_redeemed", + wantType: "tokenAlreadyRedeemed", }, { name: "share not found", err: &authguest.RedeemError{ErrorType: authguest.ErrShareNotFound}, wantStatus: http.StatusNotFound, - wantType: "share_not_found", + wantType: "shareNotFound", }, { name: "share expired", err: &authguest.RedeemError{ErrorType: authguest.ErrShareExpired}, wantStatus: http.StatusGone, - wantType: "share_expired", + wantType: "shareExpired", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { svcMock := mocks.NewAuthGuest(t) - svcMock.On("Redeem", mock.Anything, "token").Return("", tt.err) + svcMock.On("Redeem", mock.Anything, "token").Return(nil, tt.err) body, err := json.Marshal(RedeemRequest{Token: "token"}) require.NoError(t, err) @@ -121,7 +125,7 @@ func TestRedeemHandlerErrorMapping(t *testing.T) { var resp errorResponse require.NoError(t, json.NewDecoder(rr.Body).Decode(&resp)) assert.Equal(t, tt.wantType, resp.ErrorType) - assert.Equal(t, tt.wantShare, resp.ShareID) + assert.Equal(t, tt.wantPermission, resp.PermissionID) }) } } @@ -136,5 +140,5 @@ func TestRedeemHandlerMalformedBody(t *testing.T) { var resp errorResponse require.NoError(t, json.NewDecoder(rr.Body).Decode(&resp)) - assert.Equal(t, "invalid_request", resp.ErrorType) + assert.Equal(t, "invalidRequest", resp.ErrorType) } diff --git a/services/auth-guest/pkg/service/authguest/mocks/auth_guest.go b/services/auth-guest/pkg/service/authguest/mocks/auth_guest.go index e917ba9035..ed8291ea94 100644 --- a/services/auth-guest/pkg/service/authguest/mocks/auth_guest.go +++ b/services/auth-guest/pkg/service/authguest/mocks/auth_guest.go @@ -7,6 +7,7 @@ package mocks import ( "context" + "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/authguest" "github.com/opencloud-eu/opencloud/services/auth-guest/pkg/service/token" mock "github.com/stretchr/testify/mock" ) @@ -158,22 +159,24 @@ func (_c *AuthGuest_CreateToken_Call) RunAndReturn(run func(ctx context.Context, } // Redeem provides a mock function for the type AuthGuest -func (_mock *AuthGuest) Redeem(ctx context.Context, tokenString string) (string, error) { +func (_mock *AuthGuest) Redeem(ctx context.Context, tokenString string) (*authguest.RedeemResponse, error) { ret := _mock.Called(ctx, tokenString) if len(ret) == 0 { panic("no return value specified for Redeem") } - var r0 string + var r0 *authguest.RedeemResponse var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok { + if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*authguest.RedeemResponse, error)); ok { return returnFunc(ctx, tokenString) } - if returnFunc, ok := ret.Get(0).(func(context.Context, string) string); ok { + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *authguest.RedeemResponse); ok { r0 = returnFunc(ctx, tokenString) } else { - r0 = ret.Get(0).(string) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*authguest.RedeemResponse) + } } if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { r1 = returnFunc(ctx, tokenString) @@ -213,12 +216,12 @@ func (_c *AuthGuest_Redeem_Call) Run(run func(ctx context.Context, tokenString s return _c } -func (_c *AuthGuest_Redeem_Call) Return(s string, err error) *AuthGuest_Redeem_Call { - _c.Call.Return(s, err) +func (_c *AuthGuest_Redeem_Call) Return(redeemResponse *authguest.RedeemResponse, err error) *AuthGuest_Redeem_Call { + _c.Call.Return(redeemResponse, err) return _c } -func (_c *AuthGuest_Redeem_Call) RunAndReturn(run func(ctx context.Context, tokenString string) (string, error)) *AuthGuest_Redeem_Call { +func (_c *AuthGuest_Redeem_Call) RunAndReturn(run func(ctx context.Context, tokenString string) (*authguest.RedeemResponse, error)) *AuthGuest_Redeem_Call { _c.Call.Return(run) return _c } diff --git a/services/auth-guest/pkg/service/authguest/service.go b/services/auth-guest/pkg/service/authguest/service.go index 5752d8de34..ec26b086c4 100644 --- a/services/auth-guest/pkg/service/authguest/service.go +++ b/services/auth-guest/pkg/service/authguest/service.go @@ -37,10 +37,16 @@ type RedeemError struct { func (e *RedeemError) Error() string { return e.ErrorType.Error() } +// RedeemResponse is the result of a successful token redemption. +type RedeemResponse struct { + SessionToken string + ShareID string +} + // AuthGuest is the domain service used by the transport and event layers. type AuthGuest interface { CreateToken(ctx context.Context, shareID string) (*token.Token, error) - Redeem(ctx context.Context, tokenString string) (string, error) + Redeem(ctx context.Context, tokenString string) (*RedeemResponse, error) CleanupShare(shareID string) error } @@ -89,25 +95,31 @@ func (s *AuthGuestService) CreateToken(ctx context.Context, shareID string) (*to return tok, nil } -// Redeem validates a token and its share and exchanges them for a session token. -func (s *AuthGuestService) Redeem(ctx context.Context, tokenString string) (string, error) { +// Redeem validates a token and its share and exchanges them for a session token +// and the share id. +func (s *AuthGuestService) Redeem(ctx context.Context, tokenString string) (*RedeemResponse, error) { rec, err := s.verifyToken(tokenString) if err != nil { - return "", err + return nil, err } if _, err := s.validateShare(ctx, rec.ShareID); err != nil { - return "", err + return nil, err } if err := s.store.Redeem(rec.ShareIDHash); err != nil { if errors.Is(err, storage.ErrAlreadyRedeemed) { - return "", &RedeemError{ErrorType: ErrAlreadyRedeemed, ShareID: rec.ShareID} + return nil, &RedeemError{ErrorType: ErrAlreadyRedeemed, ShareID: rec.ShareID} } - return "", err + return nil, err } - return s.jwtService.Sign(rec.ShareID) + sessionToken, err := s.jwtService.Sign(rec.ShareID) + if err != nil { + return nil, err + } + + return &RedeemResponse{SessionToken: sessionToken, ShareID: rec.ShareID}, nil } // CleanupShare removes a share's token record from storage. Missing records are ignored. diff --git a/services/auth-guest/pkg/service/authguest/service_test.go b/services/auth-guest/pkg/service/authguest/service_test.go index 18595a926c..64ca7a4e4e 100644 --- a/services/auth-guest/pkg/service/authguest/service_test.go +++ b/services/auth-guest/pkg/service/authguest/service_test.go @@ -229,9 +229,10 @@ func TestRedeem(t *testing.T) { Share: share, })) - sessionToken, err := s.Redeem(context.Background(), tok) + result, err := s.Redeem(context.Background(), tok) require.NoError(t, err) - require.NotEmpty(t, sessionToken) + require.NotEmpty(t, result.SessionToken) + assert.Equal(t, testShareID, result.ShareID) store.AssertCalled(t, "Redeem", rec.ShareIDHash) } diff --git a/services/auth-guest/pkg/service/jwt/jwt.go b/services/auth-guest/pkg/service/jwt/jwt.go index 1ba9e95f9e..1c6dc0c45d 100644 --- a/services/auth-guest/pkg/service/jwt/jwt.go +++ b/services/auth-guest/pkg/service/jwt/jwt.go @@ -10,7 +10,7 @@ import ( ) type jwtClaims struct { - ShareID string `json:"share_id"` + ShareID string `json:"permissionId"` jwt.RegisteredClaims } From a1a8221f683d0913e187f52465cbe727626a09dc Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Thu, 1 Oct 2026 09:25:52 +0200 Subject: [PATCH 28/32] feat(guestauth): global flag to disable the guest links feature --- opencloud/pkg/runtime/service/service.go | 12 +++++++----- pkg/shared/shared_types.go | 1 + services/auth-guest/README.md | 2 +- services/auth-guest/pkg/server/http/server.go | 2 +- services/graph/pkg/config/config.go | 15 +++++++-------- .../graph/pkg/config/defaults/defaultconfig.go | 3 +-- .../pkg/service/v0/api_driveitem_permissions.go | 2 +- .../service/v0/api_driveitem_permissions_test.go | 3 ++- .../proxy/pkg/config/defaults/defaultconfig.go | 2 +- 9 files changed, 22 insertions(+), 20 deletions(-) diff --git a/opencloud/pkg/runtime/service/service.go b/opencloud/pkg/runtime/service/service.go index 432ef703ca..6312b1cb0f 100644 --- a/opencloud/pkg/runtime/service/service.go +++ b/opencloud/pkg/runtime/service/service.go @@ -199,11 +199,13 @@ func NewService(ctx context.Context, options ...Option) (*Service, error) { cfg.Groups.Commons = cfg.Commons return groups.Execute(cfg.Groups) }) - reg(3, opts.Config.AuthGuest.Service.Name, func(ctx context.Context, cfg *occfg.Config) error { - cfg.AuthGuest.Context = ctx - cfg.AuthGuest.Commons = cfg.Commons - return authguest.Execute(cfg.AuthGuest) - }) + if opts.Config.Commons != nil && opts.Config.Commons.EnableGuestLinks { + reg(3, opts.Config.AuthGuest.Service.Name, func(ctx context.Context, cfg *occfg.Config) error { + cfg.AuthGuest.Context = ctx + cfg.AuthGuest.Commons = cfg.Commons + return authguest.Execute(cfg.AuthGuest) + }) + } reg(3, opts.Config.IDM.Service.Name, func(ctx context.Context, cfg *occfg.Config) error { cfg.IDM.Context = ctx cfg.IDM.Commons = cfg.Commons diff --git a/pkg/shared/shared_types.go b/pkg/shared/shared_types.go index e817b75bf8..3ddd137277 100644 --- a/pkg/shared/shared_types.go +++ b/pkg/shared/shared_types.go @@ -80,6 +80,7 @@ type Commons struct { SystemUserAPIKey string `mask:"password" yaml:"system_user_api_key" env:"SYSTEM_USER_API_KEY" desc:"API key for all system users." introductionVersion:"1.0.0"` AdminUserID string `yaml:"admin_user_id" env:"OC_ADMIN_USER_ID" desc:"ID of a user, that should receive admin privileges. Consider that the UUID can be encoded in some LDAP deployment configurations like in .ldif files. These need to be decoded beforehand." introductionVersion:"1.0.0"` MultiTenantEnabled bool `yaml:"multi_tenant_enabled" env:"OC_MULTI_TENANT_ENABLED" desc:"Set this to true to enable multi-tenant support." introductionVersion:"4.0.0"` + EnableGuestLinks bool `yaml:"enable_guest_links" env:"OC_ENABLE_GUEST_LINKS" desc:"Enables the guest links feature: creating shares to mail addresses and redeeming guest link invitations. Disabled by default." introductionVersion:"%%NEXT%%"` // NOTE: you will not fing GRPCMaxReceivedMessageSize size being used in the code. The envvar is actually extracted in revas `pool` package: https://github.com/cs3org/reva/blob/edge/pkg/rgrpc/todo/pool/connection.go // It is mentioned here again so it is documented diff --git a/services/auth-guest/README.md b/services/auth-guest/README.md index 5956dadc97..6e9ee70ff8 100644 --- a/services/auth-guest/README.md +++ b/services/auth-guest/README.md @@ -27,7 +27,7 @@ It is part of the default service set and does not need to be enabled with the hash of the share id. It then publishes the `GuestTokenCreated` event with the token. 2. **Redeem** — the guest posts the token to - `POST /graph/v1beta1/extensions/org.libregraph/guestInvitations/redeem`. + `POST /graph/v1beta1/extensions/org.libregraph/guestLinks/redeem`. The service validates the token and the share, marks the token as used and returns a signed JWT session token in a cookie plus the share's `permissionId` in the response body. Tokens are single-use. diff --git a/services/auth-guest/pkg/server/http/server.go b/services/auth-guest/pkg/server/http/server.go index 6624523ff4..8e1b3c892d 100644 --- a/services/auth-guest/pkg/server/http/server.go +++ b/services/auth-guest/pkg/server/http/server.go @@ -59,7 +59,7 @@ func Server(opts ...Option) (ohttp.Service, error) { mux.Use(middlewares...) mux.Route(options.Config.HTTP.Root, func(r chi.Router) { - r.Post("/v1beta1/extensions/org.libregraph/guestInvitations/redeem", RedeemHandler(options.Logger, options.Service, options.Config)) + r.Post("/v1beta1/extensions/org.libregraph/guestLinks/redeem", RedeemHandler(options.Logger, options.Service, options.Config)) }) err = micro.RegisterHandler(newService.Server(), mux) diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index a40d17a919..a1d0b16629 100644 --- a/services/graph/pkg/config/config.go +++ b/services/graph/pkg/config/config.go @@ -26,14 +26,13 @@ type Config struct { TokenManager *TokenManager `yaml:"token_manager"` GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"` - Application Application `yaml:"application"` - Spaces Spaces `yaml:"spaces"` - Identity Identity `yaml:"identity"` - IncludeOCMSharees bool `yaml:"include_ocm_sharees" env:"OC_ENABLE_OCM;GRAPH_INCLUDE_OCM_SHAREES" desc:"Include OCM sharees when listing users." introductionVersion:"1.0.0"` - EnableGuestInvites bool `yaml:"enable_guest_invites" env:"GRAPH_ENABLE_GUEST_INVITES" desc:"Enables creating permission invites (shares) to mail addresses. Disabled by default." introductionVersion:"8.1.0"` - Events Events `yaml:"events"` - UnifiedRoles UnifiedRoles `yaml:"unified_roles"` - MaxConcurrency int `yaml:"max_concurrency" env:"OC_MAX_CONCURRENCY;GRAPH_MAX_CONCURRENCY" desc:"The maximum number of concurrent requests the service will handle." introductionVersion:"1.0.0"` + Application Application `yaml:"application"` + Spaces Spaces `yaml:"spaces"` + Identity Identity `yaml:"identity"` + IncludeOCMSharees bool `yaml:"include_ocm_sharees" env:"OC_ENABLE_OCM;GRAPH_INCLUDE_OCM_SHAREES" desc:"Include OCM sharees when listing users." introductionVersion:"1.0.0"` + Events Events `yaml:"events"` + UnifiedRoles UnifiedRoles `yaml:"unified_roles"` + MaxConcurrency int `yaml:"max_concurrency" env:"OC_MAX_CONCURRENCY;GRAPH_MAX_CONCURRENCY" desc:"The maximum number of concurrent requests the service will handle." introductionVersion:"1.0.0"` Keycloak Keycloak `yaml:"keycloak"` ServiceAccount ServiceAccount `yaml:"service_account"` diff --git a/services/graph/pkg/config/defaults/defaultconfig.go b/services/graph/pkg/config/defaults/defaultconfig.go index 598e4a099e..b7ec0af936 100644 --- a/services/graph/pkg/config/defaults/defaultconfig.go +++ b/services/graph/pkg/config/defaults/defaultconfig.go @@ -75,8 +75,7 @@ func DefaultConfig() *config.Config { AssignDefaultUserRole: true, IdentitySearchMinLength: 3, }, - EnableGuestInvites: false, - Reva: shared.DefaultRevaConfig(), + Reva: shared.DefaultRevaConfig(), Spaces: config.Spaces{ StorageUsersAddress: "eu.opencloud.api.storage-users", WebDavBase: "https://localhost:9200", diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions.go b/services/graph/pkg/service/v0/api_driveitem_permissions.go index 3e9914916a..e19eec5aa8 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions.go @@ -164,7 +164,7 @@ func (s DriveItemPermissionsService) Invite(ctx context.Context, resourceId *sto var expiration *types.Timestamp var cTime *types.Timestamp if email := driveRecipient.GetEmail(); email != "" { - if !s.config.EnableGuestInvites { + if s.config.Commons == nil || !s.config.Commons.EnableGuestLinks { return libregraph.Permission{}, errorcode.New(errorcode.NotSupported, "sharing with mail recipients is not enabled") } createShareRequest := createShareRequestToMail(email, statResponse.GetInfo(), cs3ResourcePermissions) diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions_test.go b/services/graph/pkg/service/v0/api_driveitem_permissions_test.go index e075a73003..5c386b69f6 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions_test.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions_test.go @@ -33,6 +33,7 @@ import ( cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/pkg/shared" "github.com/opencloud-eu/opencloud/services/graph/mocks" "github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults" "github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode" @@ -168,7 +169,7 @@ var _ = Describe("DriveItemPermissionsService", func() { }) It("creates guest share using an email address", func() { - cfg.EnableGuestInvites = true + cfg.Commons = &shared.Commons{EnableGuestLinks: true} gatewayClient.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil) gatewayClient.On("CreateShare", mock.Anything, mock.Anything).Return(createShareResponse, nil) driveItemInvite.Recipients = []libregraph.DriveRecipient{ diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index 68d9273ccc..457e150197 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -284,7 +284,7 @@ func DefaultPolicies() []config.Policy { Service: "eu.opencloud.web.invitations", }, { - Endpoint: "/graph/v1beta1/extensions/org.libregraph/guestInvitations", + Endpoint: "/graph/v1beta1/extensions/org.libregraph/guestLinks", Service: "eu.opencloud.web.auth-guest", Unprotected: true, }, From c5879ca1b2b551a8e571438be8659fdb2d0bfdec Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Thu, 1 Oct 2026 11:11:57 +0200 Subject: [PATCH 29/32] feat(guestauth): renaming guest ivitations to guest links for consistency with libre-graph-api --- pkg/shared/shared_types.go | 2 +- services/auth-guest/README.md | 8 ++++---- services/auth-guest/pkg/service/authguest/service.go | 4 ++-- services/auth-guest/pkg/service/authguest/service_test.go | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/shared/shared_types.go b/pkg/shared/shared_types.go index 3ddd137277..cb342014e5 100644 --- a/pkg/shared/shared_types.go +++ b/pkg/shared/shared_types.go @@ -80,7 +80,7 @@ type Commons struct { SystemUserAPIKey string `mask:"password" yaml:"system_user_api_key" env:"SYSTEM_USER_API_KEY" desc:"API key for all system users." introductionVersion:"1.0.0"` AdminUserID string `yaml:"admin_user_id" env:"OC_ADMIN_USER_ID" desc:"ID of a user, that should receive admin privileges. Consider that the UUID can be encoded in some LDAP deployment configurations like in .ldif files. These need to be decoded beforehand." introductionVersion:"1.0.0"` MultiTenantEnabled bool `yaml:"multi_tenant_enabled" env:"OC_MULTI_TENANT_ENABLED" desc:"Set this to true to enable multi-tenant support." introductionVersion:"4.0.0"` - EnableGuestLinks bool `yaml:"enable_guest_links" env:"OC_ENABLE_GUEST_LINKS" desc:"Enables the guest links feature: creating shares to mail addresses and redeeming guest link invitations. Disabled by default." introductionVersion:"%%NEXT%%"` + EnableGuestLinks bool `yaml:"enable_guest_links" env:"OC_ENABLE_GUEST_LINKS" desc:"Enables the guest links feature: creating shares to mail addresses and redeeming guest links. Disabled by default." introductionVersion:"%%NEXT%%"` // NOTE: you will not fing GRPCMaxReceivedMessageSize size being used in the code. The envvar is actually extracted in revas `pool` package: https://github.com/cs3org/reva/blob/edge/pkg/rgrpc/todo/pool/connection.go // It is mentioned here again so it is documented diff --git a/services/auth-guest/README.md b/services/auth-guest/README.md index 6e9ee70ff8..b2bce961c7 100644 --- a/services/auth-guest/README.md +++ b/services/auth-guest/README.md @@ -2,7 +2,7 @@ The `auth-guest` service gives guest users access to a share without a full OpenCloud account. When a share is created for a user of type -`USER_TYPE_GUEST`, the service issues a one-time invitation token; redeeming +`USER_TYPE_GUEST`, the service issues a one-time guest link token; redeeming that token exchanges it for a signed session cookie that authenticates the guest. @@ -13,8 +13,8 @@ It is part of the default service set and does not need to be enabled with - **Consumes** the share lifecycle events `ShareCreated`, `ShareRemoved` and `ShareExpired`. -- **Publishes** the `GuestTokenCreated` event carrying the invitation token, - so the invitation can be delivered to the guest. +- **Publishes** the `GuestTokenCreated` event carrying the guest link token, + so the link can be delivered to the guest. - Exposes an unauthenticated endpoint that redeems the token and sets a session cookie. - Stores only hashes of the token and deletes the stored record when the share @@ -47,7 +47,7 @@ Relevant options: - `AUTH_GUEST_JWT_SECRET` — secret used to sign session tokens. - `AUTH_GUEST_JWT_COOKIE_NAME`, `AUTH_GUEST_JWT_TTL` — session cookie name and lifetime. -- `AUTH_GUEST_TOKENS_STORAGE_ROOT` — where invitation token records are stored. +- `AUTH_GUEST_TOKENS_STORAGE_ROOT` — where guest link token records are stored. - `AUTH_GUEST_SERVICE_ACCOUNT_ID`, `AUTH_GUEST_SERVICE_ACCOUNT_SECRET` — service account used to query the gateway for share metadata. - `AUTH_GUEST_NUM_CONSUMERS` — number of concurrent event consumers. diff --git a/services/auth-guest/pkg/service/authguest/service.go b/services/auth-guest/pkg/service/authguest/service.go index ec26b086c4..c33e016871 100644 --- a/services/auth-guest/pkg/service/authguest/service.go +++ b/services/auth-guest/pkg/service/authguest/service.go @@ -26,7 +26,7 @@ var ErrAlreadyRedeemed = errors.New("token already redeemed") var ErrShareNotFound = errors.New("share not found") var ErrShareExpired = errors.New("share expired") -const invitationTokenTTL = 30 * time.Minute +const guestLinkTokenTTL = 30 * time.Minute // RedeemError wraps a redeem failure together with the share id. The HTTP // transport inspects ErrorType to choose a status code and message. @@ -86,7 +86,7 @@ func (s *AuthGuestService) CreateToken(ctx context.Context, shareID string) (*to ShareID: shareID, ShareIDHash: tok.ShareIDHash, SecretHash: tok.SecretHash(), - Expiry: time.Now().Add(invitationTokenTTL), + Expiry: time.Now().Add(guestLinkTokenTTL), Redeemed: false, }); err != nil { return nil, err diff --git a/services/auth-guest/pkg/service/authguest/service_test.go b/services/auth-guest/pkg/service/authguest/service_test.go index 64ca7a4e4e..df033dc596 100644 --- a/services/auth-guest/pkg/service/authguest/service_test.go +++ b/services/auth-guest/pkg/service/authguest/service_test.go @@ -115,7 +115,7 @@ func TestCreateTokenPersistsRecord(t *testing.T) { assert.Equal(t, testShareID, added.ShareID) assert.Equal(t, tok.ShareIDHash, added.ShareIDHash) assert.Equal(t, tok.SecretHash(), added.SecretHash) - assert.WithinDuration(t, time.Now().Add(invitationTokenTTL), added.Expiry, time.Minute) + assert.WithinDuration(t, time.Now().Add(guestLinkTokenTTL), added.Expiry, time.Minute) assert.False(t, added.Redeemed) } From 2c898250a9c52e776898c980d69a9444ee7a8e2d Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Thu, 1 Oct 2026 15:42:45 +0200 Subject: [PATCH 30/32] feat(guestauth): upd file manager lock, addtional filetrs for shareExpired and shareRemoved events, common config loading fix --- pkg/config/parser/parse.go | 6 ++-- .../auth-guest/pkg/service/events/handlers.go | 10 ++++++ .../pkg/service/events/handlers_test.go | 34 +++++++++++++++++-- .../pkg/service/storage/file_manager.go | 24 ++++++++----- 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/pkg/config/parser/parse.go b/pkg/config/parser/parse.go index 52e971636e..f67b68e862 100644 --- a/pkg/config/parser/parse.go +++ b/pkg/config/parser/parse.go @@ -58,15 +58,15 @@ func EnsureDefaults(cfg *config.Config) { if cfg.Reva == nil { cfg.Reva = &shared.Reva{} } -} -// EnsureCommons copies applicable parts of the OpenCloud config into the commons part -func EnsureCommons(cfg *config.Config) { // ensure the commons part is initialized if cfg.Commons == nil { cfg.Commons = &shared.Commons{} } +} +// EnsureCommons copies applicable parts of the OpenCloud config into the commons part +func EnsureCommons(cfg *config.Config) { cfg.Commons.Log = structs.CopyOrZeroValue(cfg.Log) cfg.Commons.Cache = structs.CopyOrZeroValue(cfg.Cache) diff --git a/services/auth-guest/pkg/service/events/handlers.go b/services/auth-guest/pkg/service/events/handlers.go index cabd003703..bdce9f0ec8 100644 --- a/services/auth-guest/pkg/service/events/handlers.go +++ b/services/auth-guest/pkg/service/events/handlers.go @@ -43,6 +43,11 @@ func (s *EventConsumer) handleShareRemoved(ctx context.Context, ev events.ShareR _, span := tracer.Start(ctx, "handleShareRemoved") defer span.End() + if ev.GranteeUserID == nil || ev.GranteeUserID.GetType() != user.UserType_USER_TYPE_GUEST { + s.log.Debug().Msg("share removed event is not for a guest, skipping") + return nil + } + s.log.Debug().Interface("event", ev).Msg("share removed event received") return s.authGuest.CleanupShare(ev.ShareID.GetOpaqueId()) @@ -53,6 +58,11 @@ func (s *EventConsumer) handleShareExpired(ctx context.Context, ev events.ShareE _, span := tracer.Start(ctx, "handleShareExpired") defer span.End() + if ev.GranteeUserID == nil || ev.GranteeUserID.GetType() != user.UserType_USER_TYPE_GUEST { + s.log.Debug().Msg("share expired event is not for a guest, skipping") + return nil + } + s.log.Debug().Interface("event", ev).Msg("share expired event received") return s.authGuest.CleanupShare(ev.ShareID.GetOpaqueId()) diff --git a/services/auth-guest/pkg/service/events/handlers_test.go b/services/auth-guest/pkg/service/events/handlers_test.go index 8c98f0df16..987336b87b 100644 --- a/services/auth-guest/pkg/service/events/handlers_test.go +++ b/services/auth-guest/pkg/service/events/handlers_test.go @@ -98,7 +98,8 @@ func TestHandleShareRemoved(t *testing.T) { svc, _ := newConsumer(t, svcMock) ev := events.ShareRemoved{ - ShareID: &collaboration.ShareId{OpaqueId: testShareID}, + ShareID: &collaboration.ShareId{OpaqueId: testShareID}, + GranteeUserID: &user.UserId{OpaqueId: "guest@example.org", Type: user.UserType_USER_TYPE_GUEST}, } require.NoError(t, svc.handleShareRemoved(context.Background(), ev)) @@ -106,16 +107,45 @@ func TestHandleShareRemoved(t *testing.T) { svcMock.AssertCalled(t, "CleanupShare", testShareID) } +func TestHandleShareRemovedSkipsNonGuest(t *testing.T) { + svcMock := mocks.NewAuthGuest(t) + svc, _ := newConsumer(t, svcMock) + + ev := events.ShareRemoved{ + ShareID: &collaboration.ShareId{OpaqueId: testShareID}, + GranteeUserID: &user.UserId{OpaqueId: "user", Type: user.UserType_USER_TYPE_PRIMARY}, + } + + require.NoError(t, svc.handleShareRemoved(context.Background(), ev)) + + svcMock.AssertNotCalled(t, "CleanupShare", mock.Anything) +} + func TestHandleShareExpired(t *testing.T) { svcMock := mocks.NewAuthGuest(t) svcMock.On("CleanupShare", testShareID).Return(nil) svc, _ := newConsumer(t, svcMock) ev := events.ShareExpired{ - ShareID: &collaboration.ShareId{OpaqueId: testShareID}, + ShareID: &collaboration.ShareId{OpaqueId: testShareID}, + GranteeUserID: &user.UserId{OpaqueId: "guest@example.org", Type: user.UserType_USER_TYPE_GUEST}, } require.NoError(t, svc.handleShareExpired(context.Background(), ev)) svcMock.AssertCalled(t, "CleanupShare", testShareID) } + +func TestHandleShareExpiredSkipsNonGuest(t *testing.T) { + svcMock := mocks.NewAuthGuest(t) + svc, _ := newConsumer(t, svcMock) + + ev := events.ShareExpired{ + ShareID: &collaboration.ShareId{OpaqueId: testShareID}, + GranteeUserID: &user.UserId{OpaqueId: "user", Type: user.UserType_USER_TYPE_PRIMARY}, + } + + require.NoError(t, svc.handleShareExpired(context.Background(), ev)) + + svcMock.AssertNotCalled(t, "CleanupShare", mock.Anything) +} diff --git a/services/auth-guest/pkg/service/storage/file_manager.go b/services/auth-guest/pkg/service/storage/file_manager.go index 828d39095b..ca65dbffdd 100644 --- a/services/auth-guest/pkg/service/storage/file_manager.go +++ b/services/auth-guest/pkg/service/storage/file_manager.go @@ -33,7 +33,7 @@ const filePerm = 0600 const minHashLength = 4 func (s *FileManager) Add(rec Record) error { - lock, err := s.lockStore() + lock, err := s.lockRecord(rec.ShareIDHash) if err != nil { return err } @@ -59,11 +59,14 @@ func (s *FileManager) Get(shareIDHash string) (Record, error) { } func (s *FileManager) Remove(shareIDHash string) error { - lock, err := s.lockStore() + lock, err := s.lockRecord(shareIDHash) if err != nil { return err } - defer func() { _ = lock.Unlock() }() + defer func() { + _ = lock.Unlock() + _ = os.Remove(lock.Path()) + }() p, err := s.path(shareIDHash) if err != nil { @@ -81,7 +84,7 @@ func (s *FileManager) Remove(shareIDHash string) error { } func (s *FileManager) Redeem(shareIDHash string) error { - lock, err := s.lockStore() + lock, err := s.lockRecord(shareIDHash) if err != nil { return err } @@ -100,12 +103,17 @@ func (s *FileManager) Redeem(shareIDHash string) error { return s.add(rec) } -func (s *FileManager) lockStore() (*flock.Flock, error) { - if err := os.MkdirAll(s.root, dirPerm); err != nil { - return nil, fmt.Errorf("could not create directory %s: %w", s.root, err) +func (s *FileManager) lockRecord(shareIDHash string) (*flock.Flock, error) { + p, err := s.path(shareIDHash) + if err != nil { + return nil, err } - lock := flock.New(filepath.Join(s.root, ".lock")) + if err := os.MkdirAll(filepath.Dir(p), dirPerm); err != nil { + return nil, fmt.Errorf("could not create directory %s: %w", filepath.Dir(p), err) + } + + lock := flock.New(p + ".lock") if err := lock.Lock(); err != nil { return nil, err } From c1dc12deeee8903eab29a3902de090b048b6fcf0 Mon Sep 17 00:00:00 2001 From: Alex Ababii Date: Thu, 1 Oct 2026 16:28:20 +0200 Subject: [PATCH 31/32] feat(guestauth): upd readme with guest links flow diagram --- services/auth-guest/README.md | 51 +++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/services/auth-guest/README.md b/services/auth-guest/README.md index b2bce961c7..1e763f7dbf 100644 --- a/services/auth-guest/README.md +++ b/services/auth-guest/README.md @@ -6,8 +6,8 @@ OpenCloud account. When a share is created for a user of type that token exchanges it for a signed session cookie that authenticates the guest. -It is part of the default service set and does not need to be enabled with -`OC_ADD_RUN_SERVICES`. +It is disabled by default. Set `OC_ENABLE_GUEST_LINKS=true` to enable the guest +links feature and start the service. ## Overview @@ -20,6 +20,53 @@ It is part of the default service set and does not need to be enabled with - Stores only hashes of the token and deletes the stored record when the share is removed or expires. +## Guest links flow + +The following sequence diagram describes the guest links flow: + +```mermaid +sequenceDiagram + autonumber + actor User as Guest user + participant Web as Web client + participant Redeem as Redeem endpoint + participant Proxy as OpenCloud proxy + participant Graph as Graph / sharedWithMe + participant DAV as WebDAV + participant Reva as Reva + + User->>Web: Open guest link with valid token + Web->>+Redeem: Redeem Token + Note right of Redeem: Validate Token + Redeem->>+Reva: Get Share + Reva->>-Redeem: Share + Note right of Redeem: Validate Share, Mark Token used + Redeem->>-Web: Set Cookie, return shareid + Note right of Web: HTTP only cookie with signed JWT (JWT lifetime 24h) + Web->>+Proxy: "/graph/me/drives/sharedWithMe" + Proxy->>+Reva: validate token extracted from JWT + Note right of Reva: Sign Reva Token for Guest User + Reva->>-Proxy: Authenticated + Proxy->>+Graph: "/graph/me/drives/sharedWithMe" + Note right of Proxy: Using Reva Token + Graph->>+Reva: Requests to ShareProvider + Reva->>-Graph: Shares + Graph->>-Proxy: driveItems (all shares for the Guest User) + Proxy->>-Web: driveItems + Note right of Web: Extracts driveItem for the specific share + Web->>+Proxy: PROPFIND (resource id extracted from driveItem) + Note right of Web: Using Cookie + Proxy->>+Reva: validate token extracted from JWT + Note right of Reva: Sign Reva Token for Guest User + Reva->>-Proxy: Authenticated + Proxy->>+DAV: PROPFIND + Note right of Proxy: Using Reva Token + DAV->>+Reva: Requests to StorageProvider + Reva->>-DAV: StorageProvider Responses + DAV->>-Proxy: PROPFIND Response + Proxy->>-Web: PROPFIND Response +``` + ## Token lifecycle 1. **Issue** — on the consumed `ShareCreated` event, where the grantee is a From aeb11b07ce7e1b2c983746833730e7f0a7532388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Tue, 6 Oct 2026 08:56:58 +0200 Subject: [PATCH 32/32] Adapt to renamed env var about guest links --- .woodpecker.star | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker.star b/.woodpecker.star index 266b8dc86d..c4c02009ca 100644 --- a/.woodpecker.star +++ b/.woodpecker.star @@ -2482,6 +2482,7 @@ def opencloudServer(storage = "decomposed", depends_on = [], deploy_type = "", e "OC_TRANSLATION_PATH": "%s/tests/config/translations" % dirs["base"], "ACTIVITYLOG_WRITE_BUFFER_DURATION": "0", # Disable write buffer so that test expectations are met in time "OC_LDAP_LOOKUP_CACHE_TTL": "0", # disable ldap lookup cache so that test fixture change are applied right away + "OC_ENABLE_GUEST_LINKS": True, # search grpc port needed for index cli tests "SEARCH_GRPC_ADDR": "0.0.0.0:9220", # debug addresses required for running services health tests @@ -2521,7 +2522,6 @@ def opencloudServer(storage = "decomposed", depends_on = [], deploy_type = "", e "WEBFINGER_DEBUG_ADDR": "0.0.0.0:9279", "STORAGE_USERS_POSIX_SCAN_DEBOUNCE_DELAY": 0, "OC_MACHINE_AUTH_API_KEY": MACHINE_AUTH_API_KEY, - "GRAPH_ENABLE_GUEST_INVITES": True, } if storage == "posix":