package command import ( "context" "crypto/tls" "fmt" "net/http" "os/signal" "time" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" chimiddleware "github.com/go-chi/chi/v5/middleware" "github.com/justinas/alice" "github.com/opencloud-eu/opencloud/pkg/config/configlog" "github.com/opencloud-eu/opencloud/pkg/generators" "github.com/opencloud-eu/opencloud/pkg/log" pkgmiddleware "github.com/opencloud-eu/opencloud/pkg/middleware" "github.com/opencloud-eu/opencloud/pkg/oidc" "github.com/opencloud-eu/opencloud/pkg/registry" "github.com/opencloud-eu/opencloud/pkg/runner" "github.com/opencloud-eu/opencloud/pkg/service/grpc" "github.com/opencloud-eu/opencloud/pkg/tracing" "github.com/opencloud-eu/opencloud/pkg/version" policiessvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/policies/v0" settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0" "github.com/opencloud-eu/opencloud/services/proxy/pkg/config" "github.com/opencloud-eu/opencloud/services/proxy/pkg/config/parser" "github.com/opencloud-eu/opencloud/services/proxy/pkg/logging" "github.com/opencloud-eu/opencloud/services/proxy/pkg/metrics" "github.com/opencloud-eu/opencloud/services/proxy/pkg/middleware" "github.com/opencloud-eu/opencloud/services/proxy/pkg/proxy" "github.com/opencloud-eu/opencloud/services/proxy/pkg/router" "github.com/opencloud-eu/opencloud/services/proxy/pkg/server/debug" proxyHTTP "github.com/opencloud-eu/opencloud/services/proxy/pkg/server/http" "github.com/opencloud-eu/opencloud/services/proxy/pkg/staticroutes" "github.com/opencloud-eu/opencloud/services/proxy/pkg/user/backend" "github.com/opencloud-eu/opencloud/services/proxy/pkg/userroles" "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" "github.com/opencloud-eu/reva/v2/pkg/signedurl" "github.com/opencloud-eu/reva/v2/pkg/store" "github.com/urfave/cli/v2" "go-micro.dev/v4/selector" microstore "go-micro.dev/v4/store" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/trace" ) // Server is the entrypoint for the server command. func Server(cfg *config.Config) *cli.Command { return &cli.Command{ Name: "server", Usage: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name), Category: "server", Before: func(c *cli.Context) error { return configlog.ReturnFatal(parser.ParseConfig(cfg)) }, Action: func(c *cli.Context) error { userInfoCache := store.Create( store.Store(cfg.OIDC.UserinfoCache.Store), store.TTL(cfg.OIDC.UserinfoCache.TTL), microstore.Nodes(cfg.OIDC.UserinfoCache.Nodes...), microstore.Database(cfg.OIDC.UserinfoCache.Database), microstore.Table(cfg.OIDC.UserinfoCache.Table), store.DisablePersistence(cfg.OIDC.UserinfoCache.DisablePersistence), store.Authentication(cfg.OIDC.UserinfoCache.AuthUsername, cfg.OIDC.UserinfoCache.AuthPassword), ) signingKeyStore := store.Create( store.Store(cfg.PreSignedURL.SigningKeys.Store), store.TTL(cfg.PreSignedURL.SigningKeys.TTL), microstore.Nodes(cfg.PreSignedURL.SigningKeys.Nodes...), microstore.Database("proxy"), microstore.Table("signing-keys"), store.Authentication(cfg.PreSignedURL.SigningKeys.AuthUsername, cfg.PreSignedURL.SigningKeys.AuthPassword), ) logger := logging.Configure(cfg.Service.Name, cfg.Log) traceProvider, err := tracing.GetServiceTraceProvider(cfg.Tracing, cfg.Service.Name) if err != nil { return err } cfg.GrpcClient, err = grpc.NewClient( append( grpc.GetClientOptions(cfg.GRPCClientTLS), grpc.WithTraceProvider(traceProvider))...) if err != nil { return err } oidcHTTPClient := &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ MinVersion: tls.VersionTLS12, InsecureSkipVerify: cfg.OIDC.Insecure, //nolint:gosec }, DisableKeepAlives: true, }, Timeout: time.Second * 10, } oidcClient := oidc.NewOIDCClient( oidc.WithAccessTokenVerifyMethod(cfg.OIDC.AccessTokenVerifyMethod), oidc.WithLogger(logger), oidc.WithHTTPClient(oidcHTTPClient), oidc.WithOidcIssuer(cfg.OIDC.Issuer), oidc.WithJWKSOptions(cfg.OIDC.JWKS), ) var cancel context.CancelFunc if cfg.Context == nil { cfg.Context, cancel = signal.NotifyContext(context.Background(), runner.StopSignals...) defer cancel() } m := metrics.New() m.BuildInfo.WithLabelValues(version.GetString()).Set(1) rp, err := proxy.NewMultiHostReverseProxy( proxy.Logger(logger), proxy.Config(cfg), ) if err != nil { return fmt.Errorf("failed to initialize reverse proxy: %w", err) } reg := registry.GetRegistry() gatewaySelector, err := pool.GatewaySelector( cfg.Reva.Address, append( cfg.Reva.GetRevaOptions(), pool.WithRegistry(reg), pool.WithTracerProvider(traceProvider), )...) if err != nil { logger.Fatal().Err(err).Msg("Failed to get gateway selector") } serviceSelector := selector.NewSelector(selector.Registry(reg)) var userProvider backend.UserBackend switch cfg.AccountBackend { case "cs3": userProvider = backend.NewCS3UserBackend( backend.WithLogger(logger), backend.WithRevaGatewaySelector(gatewaySelector), backend.WithSelector(serviceSelector), backend.WithMachineAuthAPIKey(cfg.MachineAuthAPIKey), backend.WithOIDCissuer(cfg.OIDC.Issuer), backend.WithServiceAccount(cfg.ServiceAccount), backend.WithAutoProvisionClaims(cfg.AutoProvisionClaims), ) default: logger.Fatal().Msgf("Invalid accounts backend type '%s'", cfg.AccountBackend) } var publisher events.Stream if cfg.Events.Endpoint != "" { var err error connName := generators.GenerateConnectionName(cfg.Service.Name, generators.NTypeBus) publisher, err = stream.NatsFromConfig(connName, false, stream.NatsConfig(cfg.Events)) if err != nil { logger.Error(). Err(err). Msg("Error initializing events publisher") return fmt.Errorf("could not initialize events publisher %w", err) } } lh := staticroutes.StaticRouteHandler{ Prefix: cfg.HTTP.Root, UserInfoCache: userInfoCache, Logger: logger, Config: *cfg, OidcClient: oidcClient, OidcHttpClient: oidcHTTPClient, Proxy: rp, EventsPublisher: publisher, UserProvider: userProvider, } if err != nil { return fmt.Errorf("failed to initialize reverse proxy: %w", err) } gr := runner.NewGroup() { middlewares := loadMiddlewares(logger, cfg, userInfoCache, signingKeyStore, traceProvider, *m, userProvider, publisher, gatewaySelector, serviceSelector) server, err := proxyHTTP.Server( proxyHTTP.Handler(lh.Handler()), proxyHTTP.Logger(logger), proxyHTTP.Context(cfg.Context), proxyHTTP.Config(cfg), proxyHTTP.Metrics(metrics.New()), proxyHTTP.Middlewares(middlewares), ) if err != nil { logger.Error(). Err(err). Str("server", "http"). Msg("Failed to initialize server") return err } gr.Add(runner.NewGoMicroHttpServerRunner(cfg.Service.Name+".http", server)) } { debugServer, err := debug.Server( debug.Logger(logger), debug.Context(cfg.Context), debug.Config(cfg), ) if err != nil { logger.Error().Err(err).Str("server", "debug").Msg("Failed to initialize server") return err } gr.Add(runner.NewGolangHttpServerRunner(cfg.Service.Name+".debug", debugServer)) } grResults := gr.Run(cfg.Context) // return the first non-nil error found in the results for _, grResult := range grResults { if grResult.RunnerError != nil { return grResult.RunnerError } } return nil }, } } func loadMiddlewares(logger log.Logger, cfg *config.Config, userInfoCache, signingKeyStore microstore.Store, traceProvider trace.TracerProvider, metrics metrics.Metrics, userProvider backend.UserBackend, publisher events.Publisher, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], serviceSelector selector.Selector) alice.Chain { rolesClient := settingssvc.NewRoleService("eu.opencloud.api.settings", cfg.GrpcClient) policiesProviderClient := policiessvc.NewPoliciesProviderService("eu.opencloud.api.policies", cfg.GrpcClient) var roleAssigner userroles.UserRoleAssigner switch cfg.RoleAssignment.Driver { case "default": roleAssigner = userroles.NewDefaultRoleAssigner( userroles.WithRoleService(rolesClient), userroles.WithLogger(logger), ) case "oidc": roleAssigner = userroles.NewOIDCRoleAssigner( userroles.WithRoleService(rolesClient), userroles.WithLogger(logger), userroles.WithRolesClaim(cfg.RoleAssignment.OIDCRoleMapper.RoleClaim), userroles.WithRoleMapping(cfg.RoleAssignment.OIDCRoleMapper.RolesMap), userroles.WithRevaGatewaySelector(gatewaySelector), userroles.WithServiceAccount(cfg.ServiceAccount), ) default: logger.Fatal().Msgf("Invalid role assignment driver '%s'", cfg.RoleAssignment.Driver) } oidcHTTPClient := &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ MinVersion: tls.VersionTLS12, InsecureSkipVerify: cfg.OIDC.Insecure, //nolint:gosec }, DisableKeepAlives: true, }, Timeout: time.Second * 10, } var authenticators []middleware.Authenticator if cfg.EnableBasicAuth { logger.Warn().Msg("basic auth enabled, use only for testing or development") authenticators = append(authenticators, middleware.BasicAuthenticator{ Logger: logger, UserProvider: userProvider, }) } if cfg.AuthMiddleware.AllowAppAuth { authenticators = append(authenticators, middleware.AppAuthAuthenticator{ Logger: logger, RevaGatewaySelector: gatewaySelector, UserRoleAssigner: roleAssigner, }) } authenticators = append(authenticators, middleware.NewOIDCAuthenticator( middleware.Logger(logger), middleware.UserInfoCache(userInfoCache), middleware.DefaultAccessTokenTTL(cfg.OIDC.UserinfoCache.TTL), middleware.HTTPClient(oidcHTTPClient), middleware.OIDCIss(cfg.OIDC.Issuer), middleware.OIDCClient(oidc.NewOIDCClient( oidc.WithAccessTokenVerifyMethod(cfg.OIDC.AccessTokenVerifyMethod), oidc.WithLogger(logger), oidc.WithHTTPClient(oidcHTTPClient), oidc.WithOidcIssuer(cfg.OIDC.Issuer), oidc.WithJWKSOptions(cfg.OIDC.JWKS), )), middleware.SkipUserInfo(cfg.OIDC.SkipUserInfo), )) authenticators = append(authenticators, middleware.PublicShareAuthenticator{ Logger: logger, RevaGatewaySelector: gatewaySelector, }) signURLVerifier, err := signedurl.NewJWTSignedURL(signedurl.WithSecret(cfg.Commons.URLSigningSecret)) if err != nil { logger.Fatal().Err(err).Msg("Failed to initialize signed URL configuration.") } authenticators = append(authenticators, middleware.SignedURLAuthenticator{ Logger: logger, PreSignedURLConfig: cfg.PreSignedURL, UserProvider: userProvider, UserRoleAssigner: roleAssigner, Store: signingKeyStore, Now: time.Now, URLVerifier: signURLVerifier, }) cspConfig, err := middleware.LoadCSPConfig(cfg) if err != nil { logger.Fatal().Err(err).Msg("Failed to load CSP configuration.") } return alice.New( chimiddleware.RealIP, chimiddleware.RequestID, // first make sure we log all requests and redirect to https if necessary otelhttp.NewMiddleware("proxy", otelhttp.WithTracerProvider(traceProvider), otelhttp.WithSpanNameFormatter(func(name string, r *http.Request) string { return fmt.Sprintf("%s %s", r.Method, r.URL.Path) }), ), middleware.Tracer(traceProvider), pkgmiddleware.TraceContext, middleware.Instrumenter(metrics), middleware.AccessLog(logger), middleware.ContextLogger(logger), middleware.HTTPSRedirect, middleware.Security(cspConfig), router.Middleware(serviceSelector, cfg.PolicySelector, cfg.Policies, logger), middleware.Authentication( authenticators, middleware.CredentialsByUserAgent(cfg.AuthMiddleware.CredentialsByUserAgent), middleware.Logger(logger), middleware.OIDCIss(cfg.OIDC.Issuer), middleware.EnableBasicAuth(cfg.EnableBasicAuth || cfg.AuthMiddleware.AllowAppAuth), middleware.TraceProvider(traceProvider), ), middleware.AccountResolver( middleware.Logger(logger), middleware.TraceProvider(traceProvider), middleware.UserProvider(userProvider), middleware.UserRoleAssigner(roleAssigner), middleware.SkipUserInfo(cfg.OIDC.SkipUserInfo), middleware.UserOIDCClaim(cfg.UserOIDCClaim), middleware.UserCS3Claim(cfg.UserCS3Claim), middleware.AutoprovisionAccounts(cfg.AutoprovisionAccounts), middleware.MultiTenantEnabled(cfg.Commons.MultiTenantEnabled), middleware.EventsPublisher(publisher), ), middleware.SelectorCookie( middleware.Logger(logger), middleware.TraceProvider(traceProvider), middleware.PolicySelectorConfig(*cfg.PolicySelector), ), middleware.Policies( cfg.PoliciesMiddleware.Query, middleware.Logger(logger), middleware.TraceProvider(traceProvider), middleware.WithRevaGatewaySelector(gatewaySelector), middleware.PoliciesProviderService(policiesProviderClient), ), // finally, trigger home creation when a user logs in middleware.CreateHome( middleware.Logger(logger), middleware.TraceProvider(traceProvider), middleware.WithRevaGatewaySelector(gatewaySelector), middleware.RoleQuotas(cfg.RoleQuotas), ), ) }