From e61206acce6876c25e2f5181f3d5dafeffb066b5 Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Fri, 9 Dec 2022 14:36:24 +0000 Subject: [PATCH 01/12] refactor(main): move grpc and http composition into internal/cmd --- cmd/flipt/main.go | 467 +------------------- internal/cleanup/cleanup.go | 2 +- internal/cleanup/cleanup_test.go | 2 +- internal/cmd/auth.go | 117 +++++ internal/cmd/grpc.go | 330 ++++++++++++++ internal/cmd/http.go | 194 ++++++++ internal/gateway/gateway.go | 32 ++ internal/server/auth/method/token/server.go | 6 + internal/server/auth/server.go | 6 + internal/server/server.go | 6 + internal/storage/sql/testing/testing.go | 19 +- 11 files changed, 728 insertions(+), 453 deletions(-) create mode 100644 internal/cmd/auth.go create mode 100644 internal/cmd/grpc.go create mode 100644 internal/cmd/http.go create mode 100644 internal/gateway/gateway.go diff --git a/cmd/flipt/main.go b/cmd/flipt/main.go index ca62b02227..b2f4b78af4 100644 --- a/cmd/flipt/main.go +++ b/cmd/flipt/main.go @@ -3,17 +3,13 @@ package main import ( "bytes" "context" - "crypto/tls" "errors" "fmt" "io/fs" - "net" - "net/http" "os" "os/signal" "path/filepath" "runtime" - "strconv" "strings" "sync" "syscall" @@ -22,64 +18,18 @@ import ( "github.com/blang/semver/v4" "github.com/fatih/color" - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "github.com/go-chi/cors" "github.com/google/go-github/v32/github" - "github.com/phyber/negroni-gzip/gzip" - "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/spf13/cobra" - "go.flipt.io/flipt/internal/cleanup" + "go.flipt.io/flipt/internal/cmd" "go.flipt.io/flipt/internal/config" "go.flipt.io/flipt/internal/info" - "go.flipt.io/flipt/internal/server" - "go.flipt.io/flipt/internal/server/auth" - authtoken "go.flipt.io/flipt/internal/server/auth/method/token" - "go.flipt.io/flipt/internal/server/cache" - "go.flipt.io/flipt/internal/server/cache/memory" - "go.flipt.io/flipt/internal/server/cache/redis" - middlewaregrpc "go.flipt.io/flipt/internal/server/middleware/grpc" - "go.flipt.io/flipt/internal/storage" - authstorage "go.flipt.io/flipt/internal/storage/auth" - authsql "go.flipt.io/flipt/internal/storage/auth/sql" - oplocksql "go.flipt.io/flipt/internal/storage/oplock/sql" "go.flipt.io/flipt/internal/storage/sql" - "go.flipt.io/flipt/internal/storage/sql/mysql" - "go.flipt.io/flipt/internal/storage/sql/postgres" - "go.flipt.io/flipt/internal/storage/sql/sqlite" "go.flipt.io/flipt/internal/telemetry" - pb "go.flipt.io/flipt/rpc/flipt" - authrpc "go.flipt.io/flipt/rpc/flipt/auth" - "go.flipt.io/flipt/swagger" - "go.flipt.io/flipt/ui" - "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/jaeger" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/sdk/resource" - tracesdk "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.4.0" - "go.opentelemetry.io/otel/trace" "go.uber.org/zap" "go.uber.org/zap/zapcore" "golang.org/x/sync/errgroup" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/reflection" - "google.golang.org/protobuf/encoding/protojson" _ "github.com/golang-migrate/migrate/v4/source/file" - - grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" - grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" - grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" - grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags" - grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" - grpc_gateway "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - - goredis_cache "github.com/go-redis/cache/v8" - goredis "github.com/go-redis/redis/v8" ) const devVersion = "dev" @@ -361,407 +311,27 @@ func run(ctx context.Context, logger *zap.Logger) error { }) } - var ( - grpcServer *grpc.Server - httpServer *http.Server - - shutdownFuncs = []func(context.Context){} - ) + grpcServer, err := cmd.NewGRPCServer(ctx, logger, forceMigrate, cfg) + if err != nil { + return err + } // starts grpc server - g.Go(func() error { - logger := logger.With(zap.String("server", "grpc")) - - migrator, err := sql.NewMigrator(*cfg, logger) - if err != nil { - return err - } - - defer migrator.Close() - - if err := migrator.Up(forceMigrate); err != nil { - return err - } - - migrator.Close() - - lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.GRPCPort)) - if err != nil { - return fmt.Errorf("creating grpc listener: %w", err) - } - - defer func() { - _ = lis.Close() - }() - - db, driver, err := sql.Open(*cfg) - if err != nil { - return fmt.Errorf("opening db: %w", err) - } - - defer db.Close() - - if err := db.PingContext(ctx); err != nil { - return fmt.Errorf("pinging db: %w", err) - } - - var store storage.Store - - switch driver { - case sql.SQLite: - store = sqlite.NewStore(db, logger) - case sql.Postgres, sql.CockroachDB: - store = postgres.NewStore(db, logger) - case sql.MySQL: - store = mysql.NewStore(db, logger) - default: - return fmt.Errorf("unsupported driver: %s", driver) - } - - logger.Debug("store enabled", zap.Stringer("driver", driver)) - - var tracingProvider = trace.NewNoopTracerProvider() - - if cfg.Tracing.Jaeger.Enabled { - logger.Debug("tracing enabled", zap.String("type", "jaeger")) - exp, err := jaeger.New(jaeger.WithAgentEndpoint( - jaeger.WithAgentHost(cfg.Tracing.Jaeger.Host), - jaeger.WithAgentPort(strconv.FormatInt(int64(cfg.Tracing.Jaeger.Port), 10)), - )) - if err != nil { - return err - } - - tracingProvider = tracesdk.NewTracerProvider( - tracesdk.WithBatcher( - exp, - tracesdk.WithBatchTimeout(1*time.Second), - ), - tracesdk.WithResource(resource.NewWithAttributes( - semconv.SchemaURL, - semconv.ServiceNameKey.String("flipt"), - )), - tracesdk.WithSampler(tracesdk.AlwaysSample()), - ) - } - - otel.SetTracerProvider(tracingProvider) - otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) - - // forward internal gRPC logging to zap - grpcLogLevel, err := zapcore.ParseLevel(cfg.Log.GRPCLevel) - if err != nil { - return fmt.Errorf("parsing grpc log level (%q): %w", cfg.Log.GRPCLevel, err) - } - - grpc_zap.ReplaceGrpcLoggerV2(logger.WithOptions(zap.IncreaseLevel(grpcLogLevel))) - - // base observability inteceptors - interceptors := []grpc.UnaryServerInterceptor{ - grpc_recovery.UnaryServerInterceptor(), - grpc_ctxtags.UnaryServerInterceptor(), - grpc_zap.UnaryServerInterceptor(logger), - grpc_prometheus.UnaryServerInterceptor, - otelgrpc.UnaryServerInterceptor(), - } - - var ( - sqlBuilder = sql.BuilderFor(db, driver) - authenticationStore = authsql.NewStore(driver, sqlBuilder, logger) - operationLockService = oplocksql.New(logger, driver, sqlBuilder) - ) - - if cfg.Authentication.ShouldRunCleanup() { - cleanupAuthService := cleanup.NewAuthenticationService(logger, operationLockService, authenticationStore, cfg.Authentication) - cleanupAuthService.Run(ctx) - - shutdownFuncs = append(shutdownFuncs, func(context.Context) { - _ = cleanupAuthService.Stop() - logger.Info("cleanup service has been shutdown") - }) - } - - // only enable enforcement middleware if authentication required - if cfg.Authentication.Required { - interceptors = append(interceptors, auth.UnaryInterceptor(logger, authenticationStore)) - - logger.Info("authentication middleware enabled") - } - - // behaviour interceptors - interceptors = append(interceptors, - middlewaregrpc.ErrorUnaryInterceptor, - middlewaregrpc.ValidationUnaryInterceptor, - middlewaregrpc.EvaluationUnaryInterceptor, - ) - - if cfg.Cache.Enabled { - var cacher cache.Cacher - - switch cfg.Cache.Backend { - case config.CacheMemory: - cacher = memory.NewCache(cfg.Cache) - case config.CacheRedis: - rdb := goredis.NewClient(&goredis.Options{ - Addr: fmt.Sprintf("%s:%d", cfg.Cache.Redis.Host, cfg.Cache.Redis.Port), - Password: cfg.Cache.Redis.Password, - DB: cfg.Cache.Redis.DB, - }) - - shutdownFuncs = append(shutdownFuncs, func(ctx context.Context) { _ = rdb.Shutdown(ctx) }) - - status := rdb.Ping(ctx) - if status == nil { - return errors.New("connecting to redis: no status") - } - - if status.Err() != nil { - return fmt.Errorf("connecting to redis: %w", status.Err()) - } - - cacher = redis.NewCache(cfg.Cache, goredis_cache.New(&goredis_cache.Options{ - Redis: rdb, - })) - } - - interceptors = append(interceptors, middlewaregrpc.CacheUnaryInterceptor(cacher, logger)) - - logger.Debug("cache enabled", zap.Stringer("backend", cacher)) - } - - grpcOpts := []grpc.ServerOption{grpc_middleware.WithUnaryServerChain(interceptors...)} - - if cfg.Server.Protocol == config.HTTPS { - creds, err := credentials.NewServerTLSFromFile(cfg.Server.CertFile, cfg.Server.CertKey) - if err != nil { - return fmt.Errorf("loading TLS credentials: %w", err) - } - - grpcOpts = append(grpcOpts, grpc.Creds(creds)) - } - - // initialize server - srv := server.New(logger, store) - // initialize grpc server - grpcServer = grpc.NewServer(grpcOpts...) - - // register grpcServer graceful stop on shutdown - shutdownFuncs = append(shutdownFuncs, func(context.Context) { - grpcServer.GracefulStop() - logger.Info("grpc server shutdown gracefully") - }) - - pb.RegisterFliptServer(grpcServer, srv) + g.Go(grpcServer.Run) - // register auth service - authServer := auth.NewServer(logger, authenticationStore) - authrpc.RegisterAuthenticationServiceServer(grpcServer, authServer) - // register auth method token service - if cfg.Authentication.Methods.Token.Enabled { - // attempt to bootstrap authentication store - clientToken, err := authstorage.Bootstrap(ctx, authenticationStore) - if err != nil { - return fmt.Errorf("configuring token authentication: %w", err) - } - - if clientToken != "" { - logger.Info("access token created", zap.String("client_token", clientToken)) - } - - tokenServer := authtoken.NewServer(logger, authenticationStore) - authrpc.RegisterAuthenticationMethodTokenServiceServer(grpcServer, tokenServer) - - logger.Debug("authentication server registered") - } - - grpc_prometheus.EnableHandlingTimeHistogram() - grpc_prometheus.Register(grpcServer) - reflection.Register(grpcServer) + // retrieve client connection to associated running gRPC server. + conn, err := grpcServer.ClientConn(ctx) + if err != nil { + return err + } - logger.Debug("starting grpc server") - return grpcServer.Serve(lis) - }) + httpServer, err := cmd.NewHTTPServer(ctx, logger, cfg, conn, info) + if err != nil { + return err + } // starts REST http(s) server - g.Go(func() error { - logger := logger.With(zap.Stringer("server", cfg.Server.Protocol)) - - var ( - // This is required to fix a backwards compatibility issue with the v2 marshaller where `null` map values - // cause an error because they are not allowed by the proto spec, but they were handled by the v1 marshaller. - // - // See: rpc/flipt/marshal.go - // - // See: https://github.com/flipt-io/flipt/issues/664 - muxOpts = []grpc_gateway.ServeMuxOption{ - grpc_gateway.WithMarshalerOption(grpc_gateway.MIMEWildcard, pb.NewV1toV2MarshallerAdapter()), - grpc_gateway.WithMarshalerOption("application/json+pretty", &grpc_gateway.JSONPb{ - MarshalOptions: protojson.MarshalOptions{ - Indent: " ", - Multiline: true, // Optional, implied by presence of "Indent". - }, - UnmarshalOptions: protojson.UnmarshalOptions{ - DiscardUnknown: true, - }, - }), - } - - r = chi.NewRouter() - api = grpc_gateway.NewServeMux(muxOpts...) - opts = []grpc.DialOption{grpc.WithBlock()} - httpPort int - ) - - switch cfg.Server.Protocol { - case config.HTTPS: - creds, err := credentials.NewClientTLSFromFile(cfg.Server.CertFile, "") - if err != nil { - return fmt.Errorf("loading TLS credentials: %w", err) - } - - opts = append(opts, grpc.WithTransportCredentials(creds)) - httpPort = cfg.Server.HTTPSPort - case config.HTTP: - opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) - httpPort = cfg.Server.HTTPPort - } - - dialCtx, dialCancel := context.WithTimeout(ctx, 5*time.Second) - defer dialCancel() - - conn, err := grpc.DialContext(dialCtx, fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.GRPCPort), opts...) - if err != nil { - return fmt.Errorf("connecting to grpc server: %w", err) - } - - if err := pb.RegisterFliptHandler(ctx, api, conn); err != nil { - return fmt.Errorf("registering grpc gateway: %w", err) - } - - if err := authrpc.RegisterAuthenticationServiceHandler(ctx, api, conn); err != nil { - return fmt.Errorf("registering auth grpc gateway: %w", err) - } - - if cfg.Authentication.Methods.Token.Enabled { - if err := authrpc.RegisterAuthenticationMethodTokenServiceHandler(ctx, api, conn); err != nil { - return fmt.Errorf("registering auth grpc gateway: %w", err) - } - } - - if cfg.Cors.Enabled { - cors := cors.New(cors.Options{ - AllowedOrigins: cfg.Cors.AllowedOrigins, - AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodOptions}, - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, - ExposedHeaders: []string{"Link"}, - AllowCredentials: true, - MaxAge: 300, - }) - - r.Use(cors.Handler) - logger.Info("CORS enabled", zap.Strings("allowed_origins", cfg.Cors.AllowedOrigins)) - } - - r.Use(middleware.RequestID) - r.Use(middleware.RealIP) - r.Use(middleware.Heartbeat("/health")) - r.Use(func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // checking Values as map[string][]string also catches ?pretty and ?pretty= - // r.URL.Query().Get("pretty") would not. - if _, ok := r.URL.Query()["pretty"]; ok { - r.Header.Set("Accept", "application/json+pretty") - } - h.ServeHTTP(w, r) - }) - }) - r.Use(middleware.Compress(gzip.DefaultCompression)) - r.Use(middleware.Recoverer) - r.Mount("/metrics", promhttp.Handler()) - r.Mount("/api/v1", api) - r.Mount("/auth/v1", api) - r.Mount("/debug", middleware.Profiler()) - - r.Route("/meta", func(r chi.Router) { - r.Use(middleware.SetHeader("Content-Type", "application/json")) - r.Handle("/info", info) - r.Handle("/config", cfg) - }) - - if cfg.UI.Enabled { - s := http.FS(swagger.Docs) - r.Mount("/docs", http.StripPrefix("/docs/", http.FileServer(s))) - - u, err := fs.Sub(ui.UI, "dist") - if err != nil { - return fmt.Errorf("mounting UI: %w", err) - } - - r.Mount("/", http.FileServer(http.FS(u))) - } - - httpServer = &http.Server{ - Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, httpPort), - Handler: r, - ReadTimeout: 10 * time.Second, - WriteTimeout: 30 * time.Second, - MaxHeaderBytes: 1 << 20, - } - - // register httpServer graceful stop on shutdown - shutdownFuncs = append(shutdownFuncs, func(ctx context.Context) { - _ = httpServer.Shutdown(ctx) - logger.Info("http server shutdown gracefully") - }) - - logger.Debug("starting http server") - - var ( - apiAddr = fmt.Sprintf("%s://%s:%d/api/v1", cfg.Server.Protocol, cfg.Server.Host, httpPort) - uiAddr = fmt.Sprintf("%s://%s:%d", cfg.Server.Protocol, cfg.Server.Host, httpPort) - ) - - if isConsole { - color.Green("\nAPI: %s", apiAddr) - - if cfg.UI.Enabled { - color.Green("UI: %s", uiAddr) - } - - fmt.Println() - } else { - logger.Info("api available", zap.String("address", apiAddr)) - - if cfg.UI.Enabled { - logger.Info("ui available", zap.String("address", uiAddr)) - } - } - - if cfg.Server.Protocol == config.HTTPS { - httpServer.TLSConfig = &tls.Config{ - MinVersion: tls.VersionTLS12, - PreferServerCipherSuites: true, - CipherSuites: []uint16{ - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - }, - } - - httpServer.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) - - err = httpServer.ListenAndServeTLS(cfg.Server.CertFile, cfg.Server.CertKey) - } else { - err = httpServer.ListenAndServe() - } - - if !errors.Is(err, http.ErrServerClosed) { - return fmt.Errorf("http server: %w", err) - } - - return nil - }) + g.Go(httpServer.Run) select { case <-interrupt: @@ -777,9 +347,8 @@ func run(ctx context.Context, logger *zap.Logger) error { shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) defer shutdownCancel() - for _, shutdown := range shutdownFuncs { - shutdown(shutdownCtx) - } + _ = httpServer.Shutdown(shutdownCtx) + _ = grpcServer.Shutdown(shutdownCtx) return g.Wait() } diff --git a/internal/cleanup/cleanup.go b/internal/cleanup/cleanup.go index 16c559dcaf..0700da6479 100644 --- a/internal/cleanup/cleanup.go +++ b/internal/cleanup/cleanup.go @@ -107,7 +107,7 @@ func (s *AuthenticationService) Run(ctx context.Context) { } // Stop signals for the cleanup goroutines to cancel and waits for them to finish. -func (s *AuthenticationService) Stop() error { +func (s *AuthenticationService) Shutdown(ctx context.Context) error { s.cancel() return s.errgroup.Wait() diff --git a/internal/cleanup/cleanup_test.go b/internal/cleanup/cleanup_test.go index b805018521..28a17fbe04 100644 --- a/internal/cleanup/cleanup_test.go +++ b/internal/cleanup/cleanup_test.go @@ -48,7 +48,7 @@ func TestCleanup(t *testing.T) { service := NewAuthenticationService(logger, lock, authstore, authConfig) service.Run(ctx) defer func() { - require.NoError(t, service.Stop()) + require.NoError(t, service.Shutdown(context.TODO())) }() } diff --git a/internal/cmd/auth.go b/internal/cmd/auth.go new file mode 100644 index 0000000000..8804e3e67d --- /dev/null +++ b/internal/cmd/auth.go @@ -0,0 +1,117 @@ +package cmd + +import ( + "context" + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "go.flipt.io/flipt/internal/cleanup" + "go.flipt.io/flipt/internal/config" + "go.flipt.io/flipt/internal/gateway" + "go.flipt.io/flipt/internal/server/auth" + authtoken "go.flipt.io/flipt/internal/server/auth/method/token" + storageauth "go.flipt.io/flipt/internal/storage/auth" + storageoplock "go.flipt.io/flipt/internal/storage/oplock" + rpcauth "go.flipt.io/flipt/rpc/flipt/auth" + "go.uber.org/zap" + "google.golang.org/grpc" +) + +func authenticationGRPC( + ctx context.Context, + logger *zap.Logger, + cfg config.AuthenticationConfig, + store storageauth.Store, + oplock storageoplock.Service, +) (GRPCRegisterers, []grpc.UnaryServerInterceptor, func(context.Context) error, error) { + var ( + register = GRPCRegisterers{ + auth.NewServer(logger, store), + } + interceptors []grpc.UnaryServerInterceptor + shutdown = func(context.Context) error { + return nil + } + ) + + // register auth method token service + if cfg.Methods.Token.Enabled { + // attempt to bootstrap authentication store + clientToken, err := storageauth.Bootstrap(ctx, store) + if err != nil { + return nil, nil, nil, fmt.Errorf("configuring token authentication: %w", err) + } + + if clientToken != "" { + logger.Info("access token created", zap.String("client_token", clientToken)) + } + + register.Add(authtoken.NewServer(logger, store)) + + logger.Debug("authentication method \"token\" server registered") + } + + // only enable enforcement middleware if authentication required + if cfg.Required { + interceptors = append(interceptors, auth.UnaryInterceptor( + logger, + store, + )) + + logger.Info("authentication middleware enabled") + } + + if cfg.ShouldRunCleanup() { + cleanupAuthService := cleanup.NewAuthenticationService( + logger, + oplock, + store, + cfg, + ) + cleanupAuthService.Run(ctx) + + shutdown = func(ctx context.Context) error { + logger.Info("shutting down authentication cleanup service...") + + return cleanupAuthService.Shutdown(ctx) + } + } + + return register, interceptors, shutdown, nil +} + +func authenticationHTTPMount( + ctx context.Context, + cfg config.AuthenticationConfig, + r chi.Router, + conn *grpc.ClientConn, +) error { + var ( + muxOpts []runtime.ServeMuxOption + middleware = func(next http.Handler) http.Handler { + return next + } + ) + + mux := gateway.NewGatewayServeMux(muxOpts...) + + if err := rpcauth.RegisterAuthenticationServiceHandler(ctx, mux, conn); err != nil { + return fmt.Errorf("registering auth grpc gateway: %w", err) + } + + if cfg.Methods.Token.Enabled { + if err := rpcauth.RegisterAuthenticationMethodTokenServiceHandler(ctx, mux, conn); err != nil { + return fmt.Errorf("registering auth grpc gateway: %w", err) + } + } + + r.Group(func(r chi.Router) { + r.Use(middleware) + + r.Mount("/auth/v1", mux) + }) + + return nil +} diff --git a/internal/cmd/grpc.go b/internal/cmd/grpc.go new file mode 100644 index 0000000000..826ad28f54 --- /dev/null +++ b/internal/cmd/grpc.go @@ -0,0 +1,330 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "net" + "strconv" + "time" + + "go.flipt.io/flipt/internal/config" + fliptserver "go.flipt.io/flipt/internal/server" + "go.flipt.io/flipt/internal/server/cache" + "go.flipt.io/flipt/internal/server/cache/memory" + "go.flipt.io/flipt/internal/server/cache/redis" + middlewaregrpc "go.flipt.io/flipt/internal/server/middleware/grpc" + "go.flipt.io/flipt/internal/storage" + authsql "go.flipt.io/flipt/internal/storage/auth/sql" + oplocksql "go.flipt.io/flipt/internal/storage/oplock/sql" + "go.flipt.io/flipt/internal/storage/sql" + "go.flipt.io/flipt/internal/storage/sql/mysql" + "go.flipt.io/flipt/internal/storage/sql/postgres" + "go.flipt.io/flipt/internal/storage/sql/sqlite" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/jaeger" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + tracesdk "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/reflection" + + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" + grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" + grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags" + grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" + + goredis_cache "github.com/go-redis/cache/v8" + goredis "github.com/go-redis/redis/v8" +) + +type GRPCRegisterer interface { + RegisterGRPC(*grpc.Server) +} + +type GRPCRegisterers []GRPCRegisterer + +func (g *GRPCRegisterers) Add(r GRPCRegisterer) { + *g = append(*g, r) +} + +func (g GRPCRegisterers) RegisterGRPC(s *grpc.Server) { + for _, register := range g { + register.RegisterGRPC(s) + } +} + +// GRPCServer configures the dependencies associated with the Flipt GRPC Service. +// It provides an entrypoint to start serving the gRPC stack (Run()). +// Along with a teardown function (Shutdown(ctx)). +type GRPCServer struct { + *grpc.Server + + logger *zap.Logger + cfg *config.Config + ln net.Listener + + shutdownFuncs []func(context.Context) error +} + +// NewGRPCServer constructs the core Flipt gRPC service including its dependencies +// (e.g. tracing, metrics, storage, migrations, caching and cleanup). +// It returns an instance of *GRPCServer which callers can Run(). +func NewGRPCServer( + ctx context.Context, + logger *zap.Logger, + forceMigrate bool, + cfg *config.Config, +) (*GRPCServer, error) { + logger = logger.With(zap.String("server", "grpc")) + server := &GRPCServer{ + logger: logger, + cfg: cfg, + } + + migrator, err := sql.NewMigrator(*cfg, logger) + if err != nil { + return nil, err + } + + if err := migrator.Up(forceMigrate); err != nil { + migrator.Close() + return nil, err + } + + migrator.Close() + + server.ln, err = net.Listen("tcp", fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.GRPCPort)) + if err != nil { + return nil, fmt.Errorf("creating grpc listener: %w", err) + } + + server.pushShutdown(func(context.Context) error { + return server.ln.Close() + }) + + db, driver, err := sql.Open(*cfg) + if err != nil { + return nil, fmt.Errorf("opening db: %w", err) + } + + server.pushShutdown(func(context.Context) error { + return db.Close() + }) + + if err := db.PingContext(ctx); err != nil { + return nil, fmt.Errorf("pinging db: %w", err) + } + + var store storage.Store + + switch driver { + case sql.SQLite: + store = sqlite.NewStore(db, logger) + case sql.Postgres, sql.CockroachDB: + store = postgres.NewStore(db, logger) + case sql.MySQL: + store = mysql.NewStore(db, logger) + default: + return nil, fmt.Errorf("unsupported driver: %s", driver) + } + + logger.Debug("store enabled", zap.Stringer("driver", driver)) + + var tracingProvider = trace.NewNoopTracerProvider() + + if cfg.Tracing.Jaeger.Enabled { + logger.Debug("tracing enabled", zap.String("type", "jaeger")) + exp, err := jaeger.New(jaeger.WithAgentEndpoint( + jaeger.WithAgentHost(cfg.Tracing.Jaeger.Host), + jaeger.WithAgentPort(strconv.FormatInt(int64(cfg.Tracing.Jaeger.Port), 10)), + )) + if err != nil { + return nil, err + } + + tracingProvider = tracesdk.NewTracerProvider( + tracesdk.WithBatcher( + exp, + tracesdk.WithBatchTimeout(1*time.Second), + ), + tracesdk.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String("flipt"), + )), + tracesdk.WithSampler(tracesdk.AlwaysSample()), + ) + } + + otel.SetTracerProvider(tracingProvider) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + + var ( + sqlBuilder = sql.BuilderFor(db, driver) + authenticationStore = authsql.NewStore(driver, sqlBuilder, logger) + operationLockService = oplocksql.New(logger, driver, sqlBuilder) + ) + + register, authInterceptors, authShutdown, err := authenticationGRPC( + ctx, + logger, + cfg.Authentication, + authenticationStore, + operationLockService, + ) + if err != nil { + return nil, err + } + + server.pushShutdown(authShutdown) + + // forward internal gRPC logging to zap + grpcLogLevel, err := zapcore.ParseLevel(cfg.Log.GRPCLevel) + if err != nil { + return nil, fmt.Errorf("parsing grpc log level (%q): %w", cfg.Log.GRPCLevel, err) + } + + grpc_zap.ReplaceGrpcLoggerV2(logger.WithOptions(zap.IncreaseLevel(grpcLogLevel))) + + // base observability inteceptors + interceptors := append([]grpc.UnaryServerInterceptor{ + grpc_recovery.UnaryServerInterceptor(), + grpc_ctxtags.UnaryServerInterceptor(), + grpc_zap.UnaryServerInterceptor(logger), + grpc_prometheus.UnaryServerInterceptor, + otelgrpc.UnaryServerInterceptor(), + }, + append(authInterceptors, + middlewaregrpc.ErrorUnaryInterceptor, + middlewaregrpc.ValidationUnaryInterceptor, + middlewaregrpc.EvaluationUnaryInterceptor, + )..., + ) + + if cfg.Cache.Enabled { + var cacher cache.Cacher + + switch cfg.Cache.Backend { + case config.CacheMemory: + cacher = memory.NewCache(cfg.Cache) + case config.CacheRedis: + rdb := goredis.NewClient(&goredis.Options{ + Addr: fmt.Sprintf("%s:%d", cfg.Cache.Redis.Host, cfg.Cache.Redis.Port), + Password: cfg.Cache.Redis.Password, + DB: cfg.Cache.Redis.DB, + }) + + server.pushShutdown(func(ctx context.Context) error { + return rdb.Shutdown(ctx).Err() + }) + + status := rdb.Ping(ctx) + if status == nil { + return nil, errors.New("connecting to redis: no status") + } + + if status.Err() != nil { + return nil, fmt.Errorf("connecting to redis: %w", status.Err()) + } + + cacher = redis.NewCache(cfg.Cache, goredis_cache.New(&goredis_cache.Options{ + Redis: rdb, + })) + } + + interceptors = append(interceptors, middlewaregrpc.CacheUnaryInterceptor(cacher, logger)) + + logger.Debug("cache enabled", zap.Stringer("backend", cacher)) + } + + grpcOpts := []grpc.ServerOption{grpc_middleware.WithUnaryServerChain(interceptors...)} + + if cfg.Server.Protocol == config.HTTPS { + creds, err := credentials.NewServerTLSFromFile(cfg.Server.CertFile, cfg.Server.CertKey) + if err != nil { + return nil, fmt.Errorf("loading TLS credentials: %w", err) + } + + grpcOpts = append(grpcOpts, grpc.Creds(creds)) + } + + // initialize server + register.Add(fliptserver.New(logger, store)) + + // initialize grpc server + server.Server = grpc.NewServer(grpcOpts...) + + // register grpcServer graceful stop on shutdown + server.pushShutdown(func(context.Context) error { + logger.Info("shutting down grpc server...") + + server.Server.GracefulStop() + + return nil + }) + + // register each grpc service onto the grpc server + register.RegisterGRPC(server.Server) + + grpc_prometheus.EnableHandlingTimeHistogram() + grpc_prometheus.Register(server.Server) + reflection.Register(server.Server) + + return server, nil +} + +// ClientConn constructs and configures a client connection to the underlying gRPC server. +func (s *GRPCServer) ClientConn(ctx context.Context) (*grpc.ClientConn, error) { + opts := []grpc.DialOption{grpc.WithBlock()} + switch s.cfg.Server.Protocol { + case config.HTTPS: + creds, err := credentials.NewClientTLSFromFile(s.cfg.Server.CertFile, "") + if err != nil { + return nil, fmt.Errorf("loading TLS credentials: %w", err) + } + + opts = append(opts, grpc.WithTransportCredentials(creds)) + case config.HTTP: + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } + + dialCtx, dialCancel := context.WithTimeout(ctx, 5*time.Second) + defer dialCancel() + + return grpc.DialContext(dialCtx, + fmt.Sprintf("%s:%d", s.cfg.Server.Host, s.cfg.Server.GRPCPort), opts...) +} + +// Run begins serving gRPC requests. +// This methods blocks until Shutdown is called. +func (s *GRPCServer) Run() error { + s.logger.Debug("starting grpc server") + + return s.Server.Serve(s.ln) +} + +// Shutdown tearsdown the entire gRPC stack including dependencies. +func (s *GRPCServer) Shutdown(ctx context.Context) error { + s.logger.Info("shutting down GRPC server...") + + // call in reverse order to emulate pop semantics of a stack + for i := len(s.shutdownFuncs) - 1; i >= 0; i-- { + if err := s.shutdownFuncs[i](ctx); err != nil { + return err + } + } + + return nil +} + +func (s *GRPCServer) pushShutdown(fn func(context.Context) error) { + s.shutdownFuncs = append(s.shutdownFuncs, fn) +} diff --git a/internal/cmd/http.go b/internal/cmd/http.go new file mode 100644 index 0000000000..1631d8fd63 --- /dev/null +++ b/internal/cmd/http.go @@ -0,0 +1,194 @@ +package cmd + +import ( + "compress/gzip" + "context" + "crypto/tls" + "errors" + "fmt" + "io/fs" + "net/http" + "time" + + "github.com/fatih/color" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.flipt.io/flipt/internal/config" + "go.flipt.io/flipt/internal/gateway" + "go.flipt.io/flipt/internal/info" + "go.flipt.io/flipt/rpc/flipt" + "go.flipt.io/flipt/swagger" + "go.flipt.io/flipt/ui" + "go.uber.org/zap" + "google.golang.org/grpc" +) + +// HTTPServer is a wrapper around the construction and registration of Flipt's HTTP server. +type HTTPServer struct { + *http.Server + + logger *zap.Logger + + listenAndServe func() error +} + +// NewHTTPServer constructs and configures the HTTPServer instance. +// The HTTPServer depends upon a running gRPC server instance which is why +// it explicitly requires and established gRPC connection as an argument. +func NewHTTPServer( + ctx context.Context, + logger *zap.Logger, + cfg *config.Config, + conn *grpc.ClientConn, + info info.Flipt, +) (*HTTPServer, error) { + logger = logger.With(zap.Stringer("server", cfg.Server.Protocol)) + + var ( + server = &HTTPServer{ + logger: logger, + } + isConsole = cfg.Log.Encoding == config.LogEncodingConsole + + r = chi.NewRouter() + api = gateway.NewGatewayServeMux() + httpPort = cfg.Server.HTTPPort + ) + + if cfg.Server.Protocol == config.HTTPS { + httpPort = cfg.Server.HTTPSPort + } + + if err := flipt.RegisterFliptHandler(ctx, api, conn); err != nil { + return nil, fmt.Errorf("registering grpc gateway: %w", err) + } + + if cfg.Cors.Enabled { + cors := cors.New(cors.Options{ + AllowedOrigins: cfg.Cors.AllowedOrigins, + AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodOptions}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: true, + MaxAge: 300, + }) + + r.Use(cors.Handler) + logger.Info("CORS enabled", zap.Strings("allowed_origins", cfg.Cors.AllowedOrigins)) + } + + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) + r.Use(middleware.Heartbeat("/health")) + r.Use(func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // checking Values as map[string][]string also catches ?pretty and ?pretty= + // r.URL.Query().Get("pretty") would not. + if _, ok := r.URL.Query()["pretty"]; ok { + r.Header.Set("Accept", "application/json+pretty") + } + h.ServeHTTP(w, r) + }) + }) + r.Use(middleware.Compress(gzip.DefaultCompression)) + r.Use(middleware.Recoverer) + r.Mount("/debug", middleware.Profiler()) + r.Mount("/metrics", promhttp.Handler()) + r.Mount("/api/v1", api) + + if err := authenticationHTTPMount(ctx, cfg.Authentication, r, conn); err != nil { + return nil, err + } + + r.Route("/meta", func(r chi.Router) { + r.Use(middleware.SetHeader("Content-Type", "application/json")) + r.Handle("/info", info) + r.Handle("/config", cfg) + }) + + if cfg.UI.Enabled { + s := http.FS(swagger.Docs) + r.Mount("/docs", http.StripPrefix("/docs/", http.FileServer(s))) + + u, err := fs.Sub(ui.UI, "dist") + if err != nil { + return nil, fmt.Errorf("mounting UI: %w", err) + } + + r.Mount("/", http.FileServer(http.FS(u))) + } + + server.Server = &http.Server{ + Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, httpPort), + Handler: r, + ReadTimeout: 10 * time.Second, + WriteTimeout: 30 * time.Second, + MaxHeaderBytes: 1 << 20, + } + + logger.Debug("starting http server") + + var ( + apiAddr = fmt.Sprintf("%s://%s:%d/api/v1", cfg.Server.Protocol, cfg.Server.Host, httpPort) + uiAddr = fmt.Sprintf("%s://%s:%d", cfg.Server.Protocol, cfg.Server.Host, httpPort) + ) + + if isConsole { + color.Green("\nAPI: %s", apiAddr) + + if cfg.UI.Enabled { + color.Green("UI: %s", uiAddr) + } + + fmt.Println() + } else { + logger.Info("api available", zap.String("address", apiAddr)) + + if cfg.UI.Enabled { + logger.Info("ui available", zap.String("address", uiAddr)) + } + } + + if cfg.Server.Protocol != config.HTTPS { + server.listenAndServe = server.ListenAndServe + return server, nil + } + + server.Server.TLSConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + PreferServerCipherSuites: true, + CipherSuites: []uint16{ + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + }, + } + + server.Server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler)) + + server.listenAndServe = func() error { + return server.ListenAndServeTLS(cfg.Server.CertFile, cfg.Server.CertKey) + } + + return server, nil +} + +// Run starts listening and serving the Flipt HTTP API. +// It blocks until the server is shutdown. +func (h *HTTPServer) Run() error { + if err := h.listenAndServe(); !errors.Is(err, http.ErrServerClosed) { + return fmt.Errorf("http server: %w", err) + } + + return nil +} + +// Shutdown triggers the shutdown operation of the HTTP API. +func (h *HTTPServer) Shutdown(ctx context.Context) error { + h.logger.Info("shutting down HTTP server...") + + return h.Server.Shutdown(ctx) +} diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go new file mode 100644 index 0000000000..cbbc7bcecf --- /dev/null +++ b/internal/gateway/gateway.go @@ -0,0 +1,32 @@ +package gateway + +import ( + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "go.flipt.io/flipt/rpc/flipt" + "google.golang.org/protobuf/encoding/protojson" +) + +// commonMuxOptions are options for gateway mux which are used for multiple instances. +// This is required to fix a backwards compatibility issue with the v2 marshaller where `null` map values +// cause an error because they are not allowed by the proto spec, but they were handled by the v1 marshaller. +// +// See: rpc/flipt/marshal.go +// +// See: https://github.com/flipt-io/flipt/issues/664 +var commonMuxOptions = []runtime.ServeMuxOption{ + runtime.WithMarshalerOption(runtime.MIMEWildcard, flipt.NewV1toV2MarshallerAdapter()), + runtime.WithMarshalerOption("application/json+pretty", &runtime.JSONPb{ + MarshalOptions: protojson.MarshalOptions{ + Indent: " ", + Multiline: true, // Optional, implied by presence of "Indent". + }, + UnmarshalOptions: protojson.UnmarshalOptions{ + DiscardUnknown: true, + }, + }), +} + +// NewGatewayServeMux builds a new gateway serve mux with common options. +func NewGatewayServeMux(opts ...runtime.ServeMuxOption) *runtime.ServeMux { + return runtime.NewServeMux(append(commonMuxOptions, opts...)...) +} diff --git a/internal/server/auth/method/token/server.go b/internal/server/auth/method/token/server.go index e6f4758c4e..8facc9736a 100644 --- a/internal/server/auth/method/token/server.go +++ b/internal/server/auth/method/token/server.go @@ -7,6 +7,7 @@ import ( storageauth "go.flipt.io/flipt/internal/storage/auth" "go.flipt.io/flipt/rpc/flipt/auth" "go.uber.org/zap" + "google.golang.org/grpc" ) const ( @@ -31,6 +32,11 @@ func NewServer(logger *zap.Logger, store storageauth.Store) *Server { } } +// RegisterGRPC registers the server as an Server on the provided grpc server. +func (s *Server) RegisterGRPC(server *grpc.Server) { + auth.RegisterAuthenticationMethodTokenServiceServer(server, s) +} + // CreateToken adapts and delegates the token request to the backing AuthenticationStore. // // Implicitly, the Authentication created will be of type auth.Method_TOKEN. diff --git a/internal/server/auth/server.go b/internal/server/auth/server.go index 3de9d877b8..78a9277493 100644 --- a/internal/server/auth/server.go +++ b/internal/server/auth/server.go @@ -8,6 +8,7 @@ import ( storageauth "go.flipt.io/flipt/internal/storage/auth" "go.flipt.io/flipt/rpc/flipt/auth" "go.uber.org/zap" + "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" ) @@ -28,6 +29,11 @@ func NewServer(logger *zap.Logger, store storageauth.Store) *Server { } } +// RegisterGRPC registers the server as an Server on the provided grpc server. +func (s *Server) RegisterGRPC(server *grpc.Server) { + auth.RegisterAuthenticationServiceServer(server, s) +} + // GetAuthenticationSelf returns the Authentication which was derived from the request context. func (s *Server) GetAuthenticationSelf(ctx context.Context, _ *emptypb.Empty) (*auth.Authentication, error) { if auth := GetAuthenticationFrom(ctx); auth != nil { diff --git a/internal/server/server.go b/internal/server/server.go index a131c5a66e..c53f07edec 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -4,6 +4,7 @@ import ( "go.flipt.io/flipt/internal/storage" flipt "go.flipt.io/flipt/rpc/flipt" "go.uber.org/zap" + "google.golang.org/grpc" ) var _ flipt.FliptServer = &Server{} @@ -22,3 +23,8 @@ func New(logger *zap.Logger, store storage.Store) *Server { store: store, } } + +// RegisterGRPC registers the *Server onto the provided grpc Server. +func (s *Server) RegisterGRPC(server *grpc.Server) { + flipt.RegisterFliptServer(server, s) +} diff --git a/internal/storage/sql/testing/testing.go b/internal/storage/sql/testing/testing.go index e304062ce2..b7037f9a6b 100644 --- a/internal/storage/sql/testing/testing.go +++ b/internal/storage/sql/testing/testing.go @@ -26,12 +26,14 @@ import ( "github.com/golang-migrate/migrate/v4/source/iofs" ) -const defaultTestDBURL = "file:../../flipt_test.db" +const defaultTestDBPrefix = "flipt_*.db" type Database struct { DB *sql.DB Driver fliptsql.Driver Container *DBContainer + + dbfile *string } func (d *Database) Shutdown(ctx context.Context) { @@ -43,6 +45,10 @@ func (d *Database) Shutdown(ctx context.Context) { _ = d.Container.StopLogProducer() _ = d.Container.Terminate(ctx) } + + if d.dbfile != nil { + os.Remove(*d.dbfile) + } } func Open() (*Database, error) { @@ -62,7 +68,7 @@ func Open() (*Database, error) { cfg := config.Config{ Database: config.DatabaseConfig{ Protocol: proto, - URL: defaultTestDBURL, + URL: createTempDBPath(), }, } @@ -288,3 +294,12 @@ type testContainerLogger struct{} func (t testContainerLogger) Accept(entry testcontainers.Log) { log.Println(entry.LogType, ":", string(entry.Content)) } + +func createTempDBPath() string { + fi, err := os.CreateTemp("", defaultTestDBPrefix) + if err != nil { + panic(err) + } + _ = fi.Close() + return "file:" + fi.Name() +} From ddb5ee412c8963dc5331bf29dd74f09286176317 Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Fri, 9 Dec 2022 14:47:15 +0000 Subject: [PATCH 02/12] chore(internal/cmd): unexport grpc register types --- internal/cmd/auth.go | 4 ++-- internal/cmd/grpc.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/cmd/auth.go b/internal/cmd/auth.go index 8804e3e67d..dd39893ff2 100644 --- a/internal/cmd/auth.go +++ b/internal/cmd/auth.go @@ -25,9 +25,9 @@ func authenticationGRPC( cfg config.AuthenticationConfig, store storageauth.Store, oplock storageoplock.Service, -) (GRPCRegisterers, []grpc.UnaryServerInterceptor, func(context.Context) error, error) { +) (grpcRegisterers, []grpc.UnaryServerInterceptor, func(context.Context) error, error) { var ( - register = GRPCRegisterers{ + register = grpcRegisterers{ auth.NewServer(logger, store), } interceptors []grpc.UnaryServerInterceptor diff --git a/internal/cmd/grpc.go b/internal/cmd/grpc.go index 826ad28f54..d2f72c2e7a 100644 --- a/internal/cmd/grpc.go +++ b/internal/cmd/grpc.go @@ -46,17 +46,17 @@ import ( goredis "github.com/go-redis/redis/v8" ) -type GRPCRegisterer interface { +type grpcRegister interface { RegisterGRPC(*grpc.Server) } -type GRPCRegisterers []GRPCRegisterer +type grpcRegisterers []grpcRegister -func (g *GRPCRegisterers) Add(r GRPCRegisterer) { +func (g *grpcRegisterers) Add(r grpcRegister) { *g = append(*g, r) } -func (g GRPCRegisterers) RegisterGRPC(s *grpc.Server) { +func (g grpcRegisterers) RegisterGRPC(s *grpc.Server) { for _, register := range g { register.RegisterGRPC(s) } From 052d192b2b9a6ca03bf226f0a611ee0d6fe933dc Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Mon, 12 Dec 2022 10:55:41 +0000 Subject: [PATCH 03/12] chore(cmd/grpc): rename pushShutdown onShutdown and move clientConn to main --- cmd/flipt/main.go | 27 ++++++++++++++++++++++++++- internal/cmd/grpc.go | 35 ++++++----------------------------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/cmd/flipt/main.go b/cmd/flipt/main.go index b2f4b78af4..8d607e2ef6 100644 --- a/cmd/flipt/main.go +++ b/cmd/flipt/main.go @@ -28,6 +28,9 @@ import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" "golang.org/x/sync/errgroup" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" _ "github.com/golang-migrate/migrate/v4/source/file" ) @@ -320,7 +323,7 @@ func run(ctx context.Context, logger *zap.Logger) error { g.Go(grpcServer.Run) // retrieve client connection to associated running gRPC server. - conn, err := grpcServer.ClientConn(ctx) + conn, err := clientConn(ctx, cfg) if err != nil { return err } @@ -399,3 +402,25 @@ func initLocalState() error { // assume state directory exists and is a directory return nil } + +// clientConn constructs and configures a client connection to the underlying gRPC server. +func clientConn(ctx context.Context, cfg *config.Config) (*grpc.ClientConn, error) { + opts := []grpc.DialOption{grpc.WithBlock()} + switch cfg.Server.Protocol { + case config.HTTPS: + creds, err := credentials.NewClientTLSFromFile(cfg.Server.CertFile, "") + if err != nil { + return nil, fmt.Errorf("loading TLS credentials: %w", err) + } + + opts = append(opts, grpc.WithTransportCredentials(creds)) + case config.HTTP: + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } + + dialCtx, dialCancel := context.WithTimeout(ctx, 5*time.Second) + defer dialCancel() + + return grpc.DialContext(dialCtx, + fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.GRPCPort), opts...) +} diff --git a/internal/cmd/grpc.go b/internal/cmd/grpc.go index d2f72c2e7a..fa11fa7967 100644 --- a/internal/cmd/grpc.go +++ b/internal/cmd/grpc.go @@ -33,7 +33,6 @@ import ( "go.uber.org/zap/zapcore" "google.golang.org/grpc" "google.golang.org/grpc/credentials" - "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/reflection" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" @@ -107,7 +106,7 @@ func NewGRPCServer( return nil, fmt.Errorf("creating grpc listener: %w", err) } - server.pushShutdown(func(context.Context) error { + server.onShutdown(func(context.Context) error { return server.ln.Close() }) @@ -116,7 +115,7 @@ func NewGRPCServer( return nil, fmt.Errorf("opening db: %w", err) } - server.pushShutdown(func(context.Context) error { + server.onShutdown(func(context.Context) error { return db.Close() }) @@ -184,7 +183,7 @@ func NewGRPCServer( return nil, err } - server.pushShutdown(authShutdown) + server.onShutdown(authShutdown) // forward internal gRPC logging to zap grpcLogLevel, err := zapcore.ParseLevel(cfg.Log.GRPCLevel) @@ -222,7 +221,7 @@ func NewGRPCServer( DB: cfg.Cache.Redis.DB, }) - server.pushShutdown(func(ctx context.Context) error { + server.onShutdown(func(ctx context.Context) error { return rdb.Shutdown(ctx).Err() }) @@ -263,7 +262,7 @@ func NewGRPCServer( server.Server = grpc.NewServer(grpcOpts...) // register grpcServer graceful stop on shutdown - server.pushShutdown(func(context.Context) error { + server.onShutdown(func(context.Context) error { logger.Info("shutting down grpc server...") server.Server.GracefulStop() @@ -281,28 +280,6 @@ func NewGRPCServer( return server, nil } -// ClientConn constructs and configures a client connection to the underlying gRPC server. -func (s *GRPCServer) ClientConn(ctx context.Context) (*grpc.ClientConn, error) { - opts := []grpc.DialOption{grpc.WithBlock()} - switch s.cfg.Server.Protocol { - case config.HTTPS: - creds, err := credentials.NewClientTLSFromFile(s.cfg.Server.CertFile, "") - if err != nil { - return nil, fmt.Errorf("loading TLS credentials: %w", err) - } - - opts = append(opts, grpc.WithTransportCredentials(creds)) - case config.HTTP: - opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) - } - - dialCtx, dialCancel := context.WithTimeout(ctx, 5*time.Second) - defer dialCancel() - - return grpc.DialContext(dialCtx, - fmt.Sprintf("%s:%d", s.cfg.Server.Host, s.cfg.Server.GRPCPort), opts...) -} - // Run begins serving gRPC requests. // This methods blocks until Shutdown is called. func (s *GRPCServer) Run() error { @@ -325,6 +302,6 @@ func (s *GRPCServer) Shutdown(ctx context.Context) error { return nil } -func (s *GRPCServer) pushShutdown(fn func(context.Context) error) { +func (s *GRPCServer) onShutdown(fn func(context.Context) error) { s.shutdownFuncs = append(s.shutdownFuncs, fn) } From 9e376d9807e83e89eda3bd6bc8aea56c337c1884 Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Thu, 1 Dec 2022 11:42:39 +0000 Subject: [PATCH 04/12] feat(oidc): implement Google OIDC provider feat(auth/oidc): implement authorize and callback refactor(oidc): switch to hashicorp/cap package test(oidc): add extra logging context for failure in CI chore: more test logging context chore: logging context around client cookie jar contents chore: print entire cookie jar fix(oidc): test use localhost over ip for cookie jar interactions chore: appears the linters refactor(internal/cmd): flatten bootstrapping package into one --- go.mod | 10 +- go.sum | 102 +++++- internal/cmd/auth.go | 23 ++ internal/config/authentication.go | 79 ++++- internal/config/config_test.go | 28 ++ internal/config/testdata/advanced.yml | 14 + internal/server/auth/method/oidc/http.go | 154 +++++++++ internal/server/auth/method/oidc/server.go | 191 +++++++++++ .../server/auth/method/oidc/server_test.go | 299 ++++++++++++++++++ .../server/auth/method/oidc/testing/grpc.go | 79 +++++ .../server/auth/method/oidc/testing/http.go | 57 ++++ 11 files changed, 1020 insertions(+), 16 deletions(-) create mode 100644 internal/server/auth/method/oidc/http.go create mode 100644 internal/server/auth/method/oidc/server.go create mode 100644 internal/server/auth/method/oidc/server_test.go create mode 100644 internal/server/auth/method/oidc/testing/grpc.go create mode 100644 internal/server/auth/method/oidc/testing/http.go diff --git a/go.mod b/go.mod index 6a9039b584..b46b0737a3 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Masterminds/squirrel v1.5.3 github.com/XSAM/otelsql v0.17.0 github.com/blang/semver/v4 v4.0.0 + github.com/coreos/go-oidc/v3 v3.4.0 github.com/docker/go-connections v0.4.0 github.com/fatih/color v1.13.0 github.com/go-chi/chi/v5 v5.0.8-0.20220103191336-b750c805b4ee @@ -21,11 +22,11 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.14.0 + github.com/hashicorp/cap v0.2.0 github.com/lib/pq v1.10.7 github.com/mattn/go-sqlite3 v1.14.16 github.com/mitchellh/mapstructure v1.5.0 github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/phyber/negroni-gzip v1.0.0 github.com/prometheus/client_golang v1.14.0 github.com/santhosh-tekuri/jsonschema/v5 v5.1.1 github.com/spf13/cobra v1.6.1 @@ -44,6 +45,7 @@ require ( go.opentelemetry.io/otel/trace v1.11.2 go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 + golang.org/x/oauth2 v0.2.0 golang.org/x/sync v0.1.0 google.golang.org/grpc v1.51.0 google.golang.org/protobuf v1.28.1 @@ -77,7 +79,10 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.2.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/klauspost/compress v1.13.6 // indirect @@ -111,7 +116,6 @@ require ( github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.4.1 // indirect github.com/uber/jaeger-lib v2.2.0+incompatible // indirect - github.com/urfave/negroni v1.0.1-0.20200608235619-7de0dfc1ff79 // indirect github.com/vmihailenco/go-tinylfu v0.2.2 // indirect github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect @@ -123,8 +127,10 @@ require ( golang.org/x/net v0.2.0 // indirect golang.org/x/sys v0.2.0 // indirect golang.org/x/text v0.4.0 // indirect + google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 8c1f9fa7e5..15004ac388 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= @@ -38,11 +40,18 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0= cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -54,6 +63,7 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= @@ -343,6 +353,9 @@ github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmeka github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-oidc/v3 v3.1.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo= +github.com/coreos/go-oidc/v3 v3.4.0 h1:xz7elHb/LDwm/ERpwHd+5nb7wFHL32rsr6bBOgaeu6g= +github.com/coreos/go-oidc/v3 v3.4.0/go.mod h1:eHUXhZtXPQLgEaDrOVTgwbgmz1xGOkJNye6h3zkD2Pw= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -418,6 +431,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -611,6 +625,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= @@ -649,13 +665,18 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= @@ -680,6 +701,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb github.com/grpc-ecosystem/grpc-gateway/v2 v2.14.0 h1:t7uX3JBHdVwAi3G7sSSdbsk8NfgA+LnUS88V/2EKaA0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.14.0/go.mod h1:4OGVnY4qf2+gw+ssiHbW+pq4mo2yko94YxxMmXZ7jCA= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hashicorp/cap v0.2.0 h1:Cgr1iDczX17y0PNF5VG+bWTtDiimYL8F18izMPbWNy4= +github.com/hashicorp/cap v0.2.0/go.mod h1:zb3VvIFA0lM2lbmO69NjowV9dJzJnZS89TaM9blXPJA= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -687,6 +710,11 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= +github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= @@ -698,6 +726,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -856,6 +886,7 @@ github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHef github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= @@ -867,6 +898,7 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= @@ -1011,8 +1043,6 @@ github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaF github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/phyber/negroni-gzip v1.0.0 h1:ru1uBeaUeoAXYgZRE7RsH7ftj/t5v/hkufXv1OYbNK8= -github.com/phyber/negroni-gzip v1.0.0/go.mod h1:poOYjiFVKpeib8SnUpOgfQGStKNGLKsM8l09lOTNeyw= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= @@ -1193,9 +1223,6 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= -github.com/urfave/negroni v1.0.1-0.20200608235619-7de0dfc1ff79 h1:310aEUwMcbpcxq9wp7hSr9f+mRV6grcai3HxdAmVl9k= -github.com/urfave/negroni v1.0.1-0.20200608235619-7de0dfc1ff79/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= @@ -1225,6 +1252,7 @@ github.com/xo/dburl v0.0.0-20200124232849-e9ec94f52bc3/go.mod h1:A47W3pdWONaZmXu github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= +github.com/yhat/scrape v0.0.0-20161128144610-24b7890b0945/go.mod h1:4vRFPPNYllgCacoj+0FoKOjTW68rUhEfqPLiEJaK2w8= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -1341,6 +1369,7 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1428,6 +1457,7 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -1454,11 +1484,18 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1480,7 +1517,12 @@ golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU= +golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1493,6 +1535,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1530,6 +1573,7 @@ golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1614,12 +1658,22 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1734,6 +1788,9 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= @@ -1772,6 +1829,15 @@ google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqiv google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1830,6 +1896,7 @@ google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= @@ -1853,8 +1920,26 @@ google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1 h1:jCw9YRd2s40X9Vxi4zKsPRvSPlHWNqadVkpbMsCPzPQ= google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= @@ -1889,7 +1974,11 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U= google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= @@ -1906,6 +1995,7 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= @@ -1933,6 +2023,8 @@ gopkg.in/segmentio/analytics-go.v3 v3.1.0/go.mod h1:4QqqlTlSSpVlWA9/9nDcPw+FkM2y gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= diff --git a/internal/cmd/auth.go b/internal/cmd/auth.go index dd39893ff2..30564bda7a 100644 --- a/internal/cmd/auth.go +++ b/internal/cmd/auth.go @@ -9,8 +9,10 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "go.flipt.io/flipt/internal/cleanup" "go.flipt.io/flipt/internal/config" + "go.flipt.io/flipt/internal/containers" "go.flipt.io/flipt/internal/gateway" "go.flipt.io/flipt/internal/server/auth" + authoidc "go.flipt.io/flipt/internal/server/auth/method/oidc" authtoken "go.flipt.io/flipt/internal/server/auth/method/token" storageauth "go.flipt.io/flipt/internal/storage/auth" storageoplock "go.flipt.io/flipt/internal/storage/oplock" @@ -53,11 +55,23 @@ func authenticationGRPC( logger.Debug("authentication method \"token\" server registered") } + var authOpts []containers.Option[auth.InterceptorOptions] + // register auth method oidc service + if cfg.Methods.OIDC.Enabled { + oidcServer := authoidc.NewServer(logger, store, cfg) + register.Add(oidcServer) + // OIDC server exposes unauthenticated endpoints + authOpts = append(authOpts, auth.WithServerSkipsAuthentication(oidcServer)) + + logger.Debug("authentication method \"oidc\" server registered") + } + // only enable enforcement middleware if authentication required if cfg.Required { interceptors = append(interceptors, auth.UnaryInterceptor( logger, store, + authOpts..., )) logger.Info("authentication middleware enabled") @@ -95,6 +109,15 @@ func authenticationHTTPMount( } ) + // register OIDC middleware if method is enabled + if cfg.Methods.OIDC.Enabled { + oidcmiddleware := authoidc.NewHTTPMiddleware(cfg.Session) + + muxOpts = append(muxOpts, + runtime.WithForwardResponseOption(oidcmiddleware.ForwardResponseOption)) + middleware = oidcmiddleware.Handler + } + mux := gateway.NewGatewayServeMux(muxOpts...) if err := rpcauth.RegisterAuthenticationServiceHandler(ctx, mux, conn); err != nil { diff --git a/internal/config/authentication.go b/internal/config/authentication.go index fb6753eb21..6cb3cbdef8 100644 --- a/internal/config/authentication.go +++ b/internal/config/authentication.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "strings" "time" @@ -31,6 +32,7 @@ type AuthenticationConfig struct { // Else, authentication is not required and Flipt's APIs are not secured. Required bool `json:"required,omitempty" mapstructure:"required"` + Session AuthenticationSession `json:"session,omitempty" mapstructure:"session"` Methods AuthenticationMethods `json:"methods,omitempty" mapstructure:"methods"` } @@ -38,26 +40,38 @@ type AuthenticationConfig struct { // It returns true given at-least 1 method is enabled and it's associated schedule // has been configured (non-nil). func (c AuthenticationConfig) ShouldRunCleanup() bool { - return (c.Methods.Token.Enabled && c.Methods.Token.Cleanup != nil) + return (c.Methods.Token.Enabled && c.Methods.Token.Cleanup != nil) || + (c.Methods.OIDC.Enabled && c.Methods.OIDC.Cleanup != nil) } func (c *AuthenticationConfig) setDefaults(v *viper.Viper) []string { - token := map[string]any{ - "enabled": false, + methods := map[string]any{ + "token": nil, + "oidc": nil, } - if v.GetBool("authentication.methods.token.enabled") { - token["cleanup"] = map[string]any{ - "interval": time.Hour, - "grace_period": 30 * time.Minute, + // set default for each methods + for k := range methods { + method := map[string]any{"enabled": false} + // if the method has been enabled then set the defaults + // for its cleanup strategy + if v.GetBool(fmt.Sprintf("authentication.methods.%s.enabled", k)) { + method["cleanup"] = map[string]any{ + "interval": time.Hour, + "grace_period": 30 * time.Minute, + } } + + methods[k] = method } v.SetDefault("authentication", map[string]any{ "required": false, - "methods": map[string]any{ - "token": token, + "session": map[string]any{ + "token_lifetime": "24h", + "state_lifetime": "10m", }, + "methods": methods, }) return nil @@ -70,6 +84,7 @@ func (c *AuthenticationConfig) validate() error { }{ // add additional schedules as token methods are created {"token", c.Methods.Token.Cleanup}, + {"oidc", c.Methods.OIDC.Cleanup}, } { if cleanup.schedule == nil { continue @@ -85,13 +100,38 @@ func (c *AuthenticationConfig) validate() error { } } + // ensure that when a session compatible authentication method has been + // enabled that the session cookie domain has been configured with a non + // empty value. + if c.Methods.OIDC.Enabled { + if c.Session.Domain == "" { + err := errFieldWrap("authentication.session.domain", errValidationRequired) + return fmt.Errorf("when session compatible auth method enabled: %w", err) + } + } + return nil } +// AuthenticationSession configures the session produced for browsers when +// establishing authentication via HTTP. +type AuthenticationSession struct { + // Domain is the domain on which to register session cookies. + Domain string `json:"domain,omitempty" mapstructure:"domain"` + // Secure sets the secure property (i.e. HTTPS only) on both the state and token cookies. + Secure bool `json:"secure" mapstructure:"secure"` + // TokenLifetime is the duration of the flipt client token generated once + // authentication has been established via a session compatible method. + TokenLifetime time.Duration `json:"tokenLifetime,omitempty" mapstructure:"token_lifetime"` + // StateLifetime is the lifetime duration of the state cookie. + StateLifetime time.Duration `json:"stateLifetime,omitempty" mapstructure:"state_lifetime"` +} + // AuthenticationMethods is a set of configuration for each authentication // method available for use within Flipt. type AuthenticationMethods struct { Token AuthenticationMethodTokenConfig `json:"token,omitempty" mapstructure:"token"` + OIDC AuthenticationMethodOIDCConfig `json:"oidc,omitempty" mapstructure:"oidc"` } // AuthenticationMethodTokenConfig contains fields used to configure the authentication @@ -105,6 +145,27 @@ type AuthenticationMethodTokenConfig struct { Cleanup *AuthenticationCleanupSchedule `json:"cleanup,omitempty" mapstructure:"cleanup"` } +// AuthenticationMethodOIDCConfig configures the OIDC authentication method. +// This method can be used to establish browser based sessions. +type AuthenticationMethodOIDCConfig struct { + Enabled bool `json:"enabled,omitempty" mapstructure:"enabled"` + Providers AuthenticationMethodOIDCProviders `json:"providers,omitempty" mapstructure:"providers"` + Cleanup *AuthenticationCleanupSchedule `json:"cleanup,omitempty" mapstructure:"cleanup"` +} + +// AuthenticationMethodOIDCProviders contains a set of providers for the OIDC authentication method. +type AuthenticationMethodOIDCProviders struct { + Google *AuthenticationMethodOIDCProviderGoogle `json:"google,omitempty" mapstructure:"google"` +} + +// AuthenticationOIDCProviderGoogle configures the Google OIDC provider credentials +type AuthenticationMethodOIDCProviderGoogle struct { + IssuerURL string `json:"issuerURL,omitempty" mapstructure:"issuer_url"` + ClientID string `json:"clientID,omitempty" mapstructure:"client_id"` + ClientSecret string `json:"clientSecret,omitempty" mapstructure:"client_secret"` + RedirectAddress string `json:"redirectAddress,omitempty" mapstructure:"redirect_address"` +} + // AuthenticationCleanupSchedule is used to configure a cleanup goroutine. type AuthenticationCleanupSchedule struct { Interval time.Duration `json:"interval,omitempty" mapstructure:"interval"` diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f70fc88422..f77f561685 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -218,6 +218,13 @@ func defaultConfig() *Config { TelemetryEnabled: true, StateDirectory: "", }, + + Authentication: AuthenticationConfig{ + Session: AuthenticationSession{ + TokenLifetime: 24 * time.Hour, + StateLifetime: 10 * time.Minute, + }, + }, } } @@ -420,6 +427,12 @@ func TestLoad(t *testing.T) { } cfg.Authentication = AuthenticationConfig{ Required: true, + Session: AuthenticationSession{ + Domain: "auth.flipt.io", + Secure: true, + TokenLifetime: 24 * time.Hour, + StateLifetime: 10 * time.Minute, + }, Methods: AuthenticationMethods{ Token: AuthenticationMethodTokenConfig{ Enabled: true, @@ -428,6 +441,21 @@ func TestLoad(t *testing.T) { GracePeriod: 48 * time.Hour, }, }, + OIDC: AuthenticationMethodOIDCConfig{ + Enabled: true, + Providers: AuthenticationMethodOIDCProviders{ + Google: &AuthenticationMethodOIDCProviderGoogle{ + IssuerURL: "http://accounts.google.com", + ClientID: "abcdefg", + ClientSecret: "bcdefgh", + RedirectAddress: "http://auth.flipt.io", + }, + }, + Cleanup: &AuthenticationCleanupSchedule{ + Interval: 2 * time.Hour, + GracePeriod: 48 * time.Hour, + }, + }, }, } return cfg diff --git a/internal/config/testdata/advanced.yml b/internal/config/testdata/advanced.yml index 24eae1f510..dcc561f105 100644 --- a/internal/config/testdata/advanced.yml +++ b/internal/config/testdata/advanced.yml @@ -42,9 +42,23 @@ meta: authentication: required: true + session: + domain: "auth.flipt.io" + secure: true methods: token: enabled: true cleanup: interval: 2h grace_period: 48h + oidc: + enabled: true + providers: + google: + issuer_url: "http://accounts.google.com" + client_id: "abcdefg" + client_secret: "bcdefgh" + redirect_address: "http://auth.flipt.io" + cleanup: + interval: 2h + grace_period: 48h diff --git a/internal/server/auth/method/oidc/http.go b/internal/server/auth/method/oidc/http.go new file mode 100644 index 0000000000..b3f13fb89c --- /dev/null +++ b/internal/server/auth/method/oidc/http.go @@ -0,0 +1,154 @@ +package oidc + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "go.flipt.io/flipt/internal/config" + "go.flipt.io/flipt/rpc/flipt/auth" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/proto" +) + +var ( + stateCookieKey = "flipt_client_state" + tokenCookieKey = "flipt_client_token" +) + +type Middleware struct { + Config config.AuthenticationSession +} + +func NewHTTPMiddleware(config config.AuthenticationSession) Middleware { + return Middleware{ + Config: config, + } +} + +// ForwardCookies forwards oidc specific cookie values onto the grpc metadata. +func ForwardCookies(ctx context.Context, req *http.Request) metadata.MD { + md := metadata.MD{} + for _, key := range []string{stateCookieKey, tokenCookieKey} { + if cookie, err := req.Cookie(key); err == nil { + md[stateCookieKey] = []string{cookie.Value} + } + } + + return md +} + +func (m Middleware) ForwardResponseOption(ctx context.Context, w http.ResponseWriter, resp proto.Message) error { + r, ok := resp.(*auth.CallbackResponse) + if ok { + cookie := &http.Cookie{ + Name: tokenCookieKey, + Value: r.ClientToken, + Domain: m.Config.Domain, + Path: "/", + Expires: time.Now().Add(m.Config.TokenLifetime), + Secure: m.Config.Secure, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + } + + http.SetCookie(w, cookie) + + // clear out token now that it is set via cookie + r.ClientToken = "" + + w.Header().Set("Location", "/") + w.WriteHeader(http.StatusFound) + } + + return nil +} + +// Handler is a http middleware used to decorate the OIDC provider gateway handler. +// The middleware intercepts authorize attempts and automatically establishes an +// appropriate state parameter. It does so by wrapping any provided state parameter +// in a JSON object with an additional cryptographically-random generated security +// token. The payload is then encoded in base64 and added back to the state query param. +// The payload is then also encoded as a http cookie which is bound to the callback path. +func (m Middleware) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + provider, method, match := parts(r.URL.Path) + if !match { + next.ServeHTTP(w, r) + return + } + + // rewrite URL paths for friendlier URL support + enumName := "OIDC_PROVIDER_" + strings.ToUpper(provider) + if _, ok := auth.OIDCProvider_value[enumName]; ok { + r.URL.Path = fmt.Sprintf("/auth/v1/method/oidc/%s/%s", enumName, method) + } + + if method == "authorize" { + query := r.URL.Query() + // create a random security token and bind it to + // the state parameter while preserving any provided + // state + v, err := json.Marshal(struct { + SecurityToken string `json:"security_token"` + OriginalState string `json:"original_state"` + }{ + // TODO(georgemac): handle redirect URL + SecurityToken: generateSecurityToken(), + // preserve and forward state + OriginalState: query.Get("state"), + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // json marshal and base64 encode payload to url-safe string + encoded := base64.URLEncoding.EncodeToString(v) + + // replace state parameter with generated value + query.Set("state", encoded) + r.URL.RawQuery = query.Encode() + + http.SetCookie(w, &http.Cookie{ + Name: stateCookieKey, + Value: encoded, + Domain: m.Config.Domain, + // bind state cookie to provider callback + Path: "/auth/v1/method/oidc/" + provider + "/callback", + Expires: time.Now().Add(m.Config.StateLifetime), + Secure: m.Config.Secure, + HttpOnly: true, + // we need to support cookie forwarding when user + // is being navigated from authorizing server + SameSite: http.SameSiteLaxMode, + }) + } + + // run decorated handler + next.ServeHTTP(w, r) + }) +} + +func parts(path string) (provider, method string, ok bool) { + const prefix = "/auth/v1/method/oidc/" + if !strings.HasPrefix(path, prefix) { + return "", "", false + } + + return strings.Cut(path[len(prefix):], "/") +} + +func generateSecurityToken() string { + var token [64]byte + if _, err := rand.Read(token[:]); err != nil { + panic(err) + } + + return base64.URLEncoding.EncodeToString(token[:]) +} diff --git a/internal/server/auth/method/oidc/server.go b/internal/server/auth/method/oidc/server.go new file mode 100644 index 0000000000..8e4f43c41c --- /dev/null +++ b/internal/server/auth/method/oidc/server.go @@ -0,0 +1,191 @@ +package oidc + +import ( + "context" + "fmt" + "time" + + "github.com/coreos/go-oidc/v3/oidc" + capoidc "github.com/hashicorp/cap/oidc" + "go.flipt.io/flipt/errors" + "go.flipt.io/flipt/internal/config" + storageauth "go.flipt.io/flipt/internal/storage/auth" + "go.flipt.io/flipt/rpc/flipt/auth" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + storageMetadataOIDCProviderKey = "io.flipt.auth.oidc.provider" + storageMetadataIDEmailKey = "io.flipt.auth.id.email" +) + +// errProviderNotFound is returned when a provider is requested which +// was not configured +var errProviderNotFound = errors.ErrNotFound("provider not found") + +type Server struct { + logger *zap.Logger + store storageauth.Store + config config.AuthenticationConfig + + auth.UnimplementedAuthenticationMethodOIDCServiceServer +} + +func NewServer( + logger *zap.Logger, + store storageauth.Store, + config config.AuthenticationConfig, +) *Server { + return &Server{ + logger: logger, + store: store, + config: config, + } +} + +// RegisterGRPC registers the server as an Server on the provided grpc server. +func (s *Server) RegisterGRPC(server *grpc.Server) { + auth.RegisterAuthenticationMethodOIDCServiceServer(server, s) +} + +// AuthorizeURL constructs and returns a URL directed at the requested OIDC provider +// based on our internal oauth2 client configuration. +// The operation is configured to return a URL which ultimately redirects to the +// callback operation below. +func (s *Server) AuthorizeURL(ctx context.Context, req *auth.AuthorizeURLRequest) (*auth.AuthorizeURLResponse, error) { + provider, oidcRequest, err := s.providerFor(req.Provider, req.State) + if err != nil { + return nil, fmt.Errorf("authorize: %w", err) + } + + // Create an auth URL + authURL, err := provider.AuthURL(context.Background(), oidcRequest) + if err != nil { + return nil, err + } + + return &auth.AuthorizeURLResponse{AuthorizeUrl: authURL}, nil +} + +// Callback attempts to authenticate a callback request from a delegated authorization service. +// The supplied state metadata compared with the request state and ensured to be equal. +// The provided code is exchanged with the associated provider for an "id_token". +// We verify the retrieved "id_token" is valid and for our client. +// Once verified we extract the users associated email address. +// Given all this completes successfully then we established an associated clientToken in +// the backing authentication store with the identity information retrieved as metadata. +func (s *Server) Callback(ctx context.Context, req *auth.CallbackRequest) (_ *auth.CallbackResponse, err error) { + defer func() { + if err != nil { + err = fmt.Errorf("handling OIDC callback: %w", err) + } + }() + + if req.State != "" { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, errors.New("missing metadata") + } + + state, ok := md["flipt_client_state"] + if !ok || len(state) < 1 { + return nil, errors.New("missing state") + } + + if req.State != state[0] { + return nil, errors.New("unexpected state") + } + } + + provider, oidcRequest, err := s.providerFor(req.Provider, req.State) + if err != nil { + return nil, err + } + + responseToken, err := provider.Exchange(ctx, oidcRequest, req.State, req.Code) + if err != nil { + return nil, err + } + + // Extract custom claims + var claims struct { + Email string `json:"email"` + Verified bool `json:"email_verified"` + } + if err := responseToken.IDToken().Claims(&claims); err != nil { + return nil, err + } + + clientToken, a, err := s.store.CreateAuthentication(ctx, &storageauth.CreateAuthenticationRequest{ + Method: auth.Method_METHOD_OIDC, + ExpiresAt: timestamppb.New(time.Now().UTC().Add(s.config.Session.TokenLifetime)), + Metadata: map[string]string{ + storageMetadataIDEmailKey: claims.Email, + storageMetadataOIDCProviderKey: req.Provider.String(), + }, + }) + if err != nil { + return nil, err + + } + + return &auth.CallbackResponse{ + ClientToken: clientToken, + Authentication: a, + }, nil +} + +func callbackURL(host, provider string) string { + return host + "/auth/v1/method/oidc/" + provider + "/callback" +} + +func (s *Server) providerFor(provider auth.OIDCProvider, state string) (*capoidc.Provider, *capoidc.Req, error) { + var ( + providerConfig = s.config.Methods.OIDC.Providers + config *capoidc.Config + callback string + scopes []string + ) + + switch provider { + case auth.OIDCProvider_OIDC_PROVIDER_GOOGLE: + // Create a new provider config + google := providerConfig.Google + + callback = callbackURL(google.RedirectAddress, "google") + scopes = []string{"profile", "email"} + + var err error + config, err = capoidc.NewConfig( + google.IssuerURL, + google.ClientID, + capoidc.ClientSecret(google.ClientSecret), + []capoidc.Alg{oidc.RS256}, + []string{callback}, + ) + if err != nil { + return nil, nil, err + } + default: + return nil, nil, fmt.Errorf("requested provider %q: %w", provider, errProviderNotFound) + } + + p, err := capoidc.NewProvider(config) + if err != nil { + return nil, nil, err + } + + req, err := capoidc.NewRequest(2*time.Minute, callback, + capoidc.WithState(state), + capoidc.WithScopes(scopes...), + capoidc.WithNonce("static"), // TODO(georgemac): dropping nonce for now + ) + if err != nil { + return nil, nil, err + } + + return p, req, nil +} diff --git a/internal/server/auth/method/oidc/server_test.go b/internal/server/auth/method/oidc/server_test.go new file mode 100644 index 0000000000..ad58c3d1d0 --- /dev/null +++ b/internal/server/auth/method/oidc/server_test.go @@ -0,0 +1,299 @@ +package oidc_test + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "fmt" + "io" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "net/url" + "os" + "strings" + "testing" + "time" + + "github.com/go-chi/chi/v5" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/cap/oidc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.flipt.io/flipt/internal/config" + oidctesting "go.flipt.io/flipt/internal/server/auth/method/oidc/testing" + "go.flipt.io/flipt/rpc/flipt/auth" + "go.uber.org/zap/zaptest" + "golang.org/x/net/html" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/testing/protocmp" +) + +func Test_Server(t *testing.T) { + var ( + router = chi.NewRouter() + // httpServer is the test server used for hosting + // Flipts oidc authorize and callback handles + httpServer = httptest.NewServer(router) + // rewriting http server to use localhost as it is a domain and + // the <=go1.18 implementation will propagate cookies on it. + // From go1.19+ cookiejar support IP addresses as cookie domains. + clientAddress = strings.Replace(httpServer.URL, "127.0.0.1", "localhost", 1) + + id, secret = "client_id", "client_secret" + + logger = zaptest.NewLogger(t) + ctx = context.Background() + ) + + priv, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n\n", err) + return + } + + tp := oidc.StartTestProvider(t, oidc.WithNoTLS(), oidc.WithTestDefaults(&oidc.TestProviderDefaults{ + CustomClaims: map[string]interface{}{}, + SubjectInfo: map[string]*oidc.TestSubject{ + "mark": { + Password: "phelps", + UserInfo: map[string]interface{}{ + "email": "mark@flipt.io", + "name": "Mark Phelps", + }, + CustomClaims: map[string]interface{}{ + "email": "mark@flipt.io", + "name": "Mark Phelps", + }, + }, + "george": { + Password: "macrorie", + UserInfo: map[string]interface{}{ + "email": "george@flipt.io", + "name": "George MacRorie", + }, + CustomClaims: map[string]interface{}{ + "email": "george@flipt.io", + "name": "George MacRorie", + }, + }, + }, + SigningKey: &oidc.TestSigningKey{ + PrivKey: priv, + PubKey: priv.Public(), + Alg: oidc.RS256, + }, + AllowedRedirectURIs: []string{ + fmt.Sprintf("%s/auth/v1/method/oidc/google/callback", clientAddress), + }, + ClientID: &id, + ClientSecret: &secret, + })) + + defer tp.Stop() + + var ( + authConfig = config.AuthenticationConfig{ + Session: config.AuthenticationSession{ + Domain: "localhost", + Secure: false, + TokenLifetime: 1 * time.Hour, + StateLifetime: 10 * time.Minute, + }, + Methods: config.AuthenticationMethods{ + OIDC: config.AuthenticationMethodOIDCConfig{ + Enabled: true, + Providers: config.AuthenticationMethodOIDCProviders{ + Google: &config.AuthenticationMethodOIDCProviderGoogle{ + IssuerURL: tp.Addr(), + ClientID: id, + ClientSecret: secret, + RedirectAddress: clientAddress, + }, + }, + }, + }, + } + server = oidctesting.StartHTTPServer(t, ctx, logger, authConfig, router) + ) + + t.Cleanup(func() { _ = server.Stop() }) + + jar, err := cookiejar.New(&cookiejar.Options{}) + require.NoError(t, err) + + client := &http.Client{ + // skip redirects + CheckRedirect: func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + }, + // establish a cookie jar + Jar: jar, + } + + var authURL *url.URL + t.Run("AuthorizeURL", func(t *testing.T) { + authorizeURL := clientAddress + "/auth/v1/method/oidc/google/authorize" + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, authorizeURL, nil) + require.NoError(t, err) + + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusOK, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + var authorize auth.AuthorizeURLResponse + err = protojson.Unmarshal(body, &authorize) + require.NoError(t, err) + + authURL, err = url.Parse(authorize.AuthorizeUrl) + require.NoError(t, err) + }) + + t.Log("Navigating to authorize URL:", authURL.String()) + + var location string + t.Run("Login as Mark", func(t *testing.T) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, authURL.String(), nil) + require.NoError(t, err) + + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + values, err := parseLoginFormHiddenValues(resp.Body) + require.NoError(t, err) + + // add login credentials + values.Set("uname", "mark") + values.Set("psw", "phelps") + + req, err = http.NewRequestWithContext(ctx, http.MethodPost, tp.Addr()+"/login", strings.NewReader(values.Encode())) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err = client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusFound, resp.StatusCode) + + location = resp.Header.Get("Location") + }) + + t.Log("Redirecting to callback URL:", location) + + t.Run("Callback", func(t *testing.T) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, location, nil) + require.NoError(t, err) + + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + var response auth.CallbackResponse + if !assert.NoError(t, protojson.Unmarshal(data, &response)) { + t.Log("Unexpected response", string(data)) + t.FailNow() + } + + assert.Empty(t, response.ClientToken) // middleware moves it to cookie + assert.Equal(t, auth.Method_METHOD_OIDC, response.Authentication.Method) + assert.Equal(t, map[string]string{ + "io.flipt.auth.id.email": "mark@flipt.io", + "io.flipt.auth.oidc.provider": "OIDC_PROVIDER_GOOGLE", + }, response.Authentication.Metadata) + + // ensure expiry is set + assert.NotNil(t, response.Authentication.ExpiresAt) + + // obtain returned cookie + cookie, err := (&http.Request{ + Header: http.Header{"Cookie": resp.Header["Set-Cookie"]}, + }).Cookie("flipt_client_token") + require.NoError(t, err) + + // check authentication in store matches + storedAuth, err := server.GRPCServer.Store.GetAuthenticationByClientToken(ctx, cookie.Value) + require.NoError(t, err) + + // ensure stored auth can be retrieved by cookie abd matches response body auth + if diff := cmp.Diff(storedAuth, response.Authentication, protocmp.Transform()); err != nil { + t.Errorf("-exp/+got:\n%s", diff) + } + }) +} + +// parseLoginFormHiddenValues parses the contents of the supplied reader as HTML. +// It descends into the document looking for the hidden values associated +// with the login form. +// It collecs the hidden values into a url.Values so that we can post the form +// using our Go client. +func parseLoginFormHiddenValues(r io.Reader) (url.Values, error) { + values := url.Values{} + + doc, err := html.Parse(r) + if err != nil { + return nil, err + } + + findNode := func(visit func(*html.Node), name string, attrs ...html.Attribute) func(*html.Node) { + var f func(*html.Node) + f = func(n *html.Node) { + var hasAttrs bool + if n.Type == html.ElementNode && n.Data == name { + hasAttrs = true + for _, want := range attrs { + for _, has := range n.Attr { + if has.Key == want.Key { + hasAttrs = hasAttrs && (want.Val == has.Val) + } + } + } + } + + if hasAttrs { + visit(n) + return + } + + for c := n.FirstChild; c != nil; c = c.NextSibling { + f(c) + } + } + + return f + } + + findNode(func(n *html.Node) { + for c := n.FirstChild; c != nil; c = c.NextSibling { + findNode(func(n *html.Node) { + var ( + name string + value string + ) + for _, a := range n.Attr { + switch a.Key { + case "name": + name = a.Val + case "value": + value = a.Val + } + } + + values.Set(name, value) + }, "input", html.Attribute{Key: "type", Val: "hidden"})(c) + } + }, "form", html.Attribute{Key: "action", Val: "/login"})(doc) + + return values, nil +} diff --git a/internal/server/auth/method/oidc/testing/grpc.go b/internal/server/auth/method/oidc/testing/grpc.go new file mode 100644 index 0000000000..d2401898e0 --- /dev/null +++ b/internal/server/auth/method/oidc/testing/grpc.go @@ -0,0 +1,79 @@ +package testing + +import ( + "context" + "net" + "testing" + + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + "github.com/stretchr/testify/require" + "go.flipt.io/flipt/internal/config" + "go.flipt.io/flipt/internal/server/auth/method/oidc" + middleware "go.flipt.io/flipt/internal/server/middleware/grpc" + "go.flipt.io/flipt/internal/storage/auth/memory" + "go.flipt.io/flipt/rpc/flipt/auth" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/test/bufconn" +) + +type GRPCServer struct { + *grpc.Server + + ClientConn *grpc.ClientConn + Store *memory.Store + + errc chan error +} + +func (s *GRPCServer) Client() auth.AuthenticationMethodOIDCServiceClient { + return auth.NewAuthenticationMethodOIDCServiceClient(s.ClientConn) +} + +func StartGRPCServer(t *testing.T, ctx context.Context, logger *zap.Logger, conf config.AuthenticationConfig) *GRPCServer { + t.Helper() + + var ( + store = memory.NewStore() + listener = bufconn.Listen(1024 * 1024) + server = grpc.NewServer( + grpc_middleware.WithUnaryServerChain( + middleware.ErrorUnaryInterceptor, + ), + ) + grpcServer = &GRPCServer{ + Server: server, + Store: store, + errc: make(chan error, 1), + } + ) + + auth.RegisterAuthenticationMethodOIDCServiceServer(server, oidc.NewServer(logger, store, conf)) + + go func() { + defer close(grpcServer.errc) + grpcServer.errc <- server.Serve(listener) + }() + + var ( + err error + dialer = func(context.Context, string) (net.Conn, error) { + return listener.Dial() + } + ) + + grpcServer.ClientConn, err = grpc.DialContext(ctx, "", grpc.WithInsecure(), grpc.WithContextDialer(dialer)) + require.NoError(t, err) + + return grpcServer +} + +func (s *GRPCServer) Stop() error { + if err := s.ClientConn.Close(); err != nil { + return err + } + + s.Server.Stop() + + return <-s.errc +} diff --git a/internal/server/auth/method/oidc/testing/http.go b/internal/server/auth/method/oidc/testing/http.go new file mode 100644 index 0000000000..1cdaa0048f --- /dev/null +++ b/internal/server/auth/method/oidc/testing/http.go @@ -0,0 +1,57 @@ +package testing + +import ( + "context" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/stretchr/testify/require" + "go.flipt.io/flipt/internal/config" + "go.flipt.io/flipt/internal/gateway" + "go.flipt.io/flipt/internal/server/auth/method/oidc" + "go.flipt.io/flipt/rpc/flipt/auth" + "go.uber.org/zap" +) + +type HTTPServer struct { + *GRPCServer +} + +func StartHTTPServer( + t *testing.T, + ctx context.Context, + logger *zap.Logger, + conf config.AuthenticationConfig, + router chi.Router, +) *HTTPServer { + t.Helper() + + var ( + httpServer = &HTTPServer{ + GRPCServer: StartGRPCServer(t, ctx, logger, conf), + } + + oidcmiddleware = oidc.NewHTTPMiddleware(conf.Session) + mux = gateway.NewGatewayServeMux( + runtime.WithMetadata(oidc.ForwardCookies), + runtime.WithForwardResponseOption(oidcmiddleware.ForwardResponseOption), + ) + ) + + err := auth.RegisterAuthenticationMethodOIDCServiceHandler( + ctx, + mux, + httpServer.GRPCServer.ClientConn, + ) + require.NoError(t, err) + + router.Use(oidcmiddleware.Handler) + router.Mount("/auth/v1", mux) + + return httpServer +} + +func (s *HTTPServer) Stop() error { + return s.GRPCServer.Stop() +} From 607b74b212729362d7e39fd40de06f5a975f881f Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Mon, 12 Dec 2022 15:08:07 +0000 Subject: [PATCH 05/12] chore(method/oidc): improve docs around http middleware --- internal/server/auth/method/oidc/http.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/internal/server/auth/method/oidc/http.go b/internal/server/auth/method/oidc/http.go index b3f13fb89c..a56697f665 100644 --- a/internal/server/auth/method/oidc/http.go +++ b/internal/server/auth/method/oidc/http.go @@ -21,17 +21,25 @@ var ( tokenCookieKey = "flipt_client_token" ) +// Middleware contains various extensions for appropriate integration of the OIDC services +// behind gRPC gateway. This includes forwarding cookies as gRPC metadata, adapting callback +// responses to http cookies, and establishing appropriate state parameters for csrf provention +// during the oauth/oidc flow. type Middleware struct { Config config.AuthenticationSession } +// NewHTTPMiddleware constructs and configures a new oidc HTTP middleware from the supplied +// authentication configuration struct. func NewHTTPMiddleware(config config.AuthenticationSession) Middleware { return Middleware{ Config: config, } } -// ForwardCookies forwards oidc specific cookie values onto the grpc metadata. +// ForwardCookies parses particular http cookies (Flipts state and client token) and +// forwards them as grpc metadata entries. This allows us to abstract away http +// constructs from the internal gRPC implementation. func ForwardCookies(ctx context.Context, req *http.Request) metadata.MD { md := metadata.MD{} for _, key := range []string{stateCookieKey, tokenCookieKey} { @@ -43,6 +51,12 @@ func ForwardCookies(ctx context.Context, req *http.Request) metadata.MD { return md } +// ForwardResponseOption is a grpc gateway forward response option function implementation. +// The purpose of which is to intercept outgoing Callback operation responses. +// When intercepted the resulting clientToken is stripped from the response payload and instead +// added to a response header cookie (Set-Cookie). +// This ensures a secure browser session can be established. +// The user-agent is then redirected to the root of the domain. func (m Middleware) ForwardResponseOption(ctx context.Context, w http.ResponseWriter, resp proto.Message) error { r, ok := resp.(*auth.CallbackResponse) if ok { From fc7a329691e0d3f235017de46296302c3cdac40a Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Mon, 12 Dec 2022 15:45:19 +0000 Subject: [PATCH 06/12] refactor(errors): add generic errors.NewErrorf --- errors/errors.go | 30 +++++++++++++++-------- internal/server/evaluator.go | 8 +++--- internal/server/evaluator_test.go | 6 ++--- internal/storage/auth/auth.go | 2 +- internal/storage/auth/memory/store.go | 6 ++--- internal/storage/sql/common/flag.go | 6 ++--- internal/storage/sql/common/rule.go | 6 ++--- internal/storage/sql/common/segment.go | 6 ++--- internal/storage/sql/mysql/mysql.go | 16 ++++++------ internal/storage/sql/postgres/postgres.go | 16 ++++++------ internal/storage/sql/sqlite/sqlite.go | 16 ++++++------ rpc/flipt/validation.go | 16 ++++++------ 12 files changed, 72 insertions(+), 62 deletions(-) diff --git a/errors/errors.go b/errors/errors.go index 50d175418a..42ea816846 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -16,14 +16,14 @@ func New(s string) error { return errors.New(s) } +// NewErrorf is a generic utility for formatting a string into a target error type E. +func NewErrorf[E StringError](format string, args ...any) error { + return E(fmt.Sprintf(format, args...)) +} + // ErrNotFound represents a not found error type ErrNotFound string -// ErrNotFoundf creates an ErrNotFound using a custom format -func ErrNotFoundf(format string, args ...interface{}) error { - return ErrNotFound(fmt.Sprintf(format, args...)) -} - func (e ErrNotFound) Error() string { return fmt.Sprintf("%s not found", string(e)) } @@ -31,11 +31,6 @@ func (e ErrNotFound) Error() string { // ErrInvalid represents an invalid error type ErrInvalid string -// ErrInvalidf creates an ErrInvalid using a custom format -func ErrInvalidf(format string, args ...interface{}) error { - return ErrInvalid(fmt.Sprintf(format, args...)) -} - func (e ErrInvalid) Error() string { return string(e) } @@ -59,3 +54,18 @@ func InvalidFieldError(field, reason string) error { func EmptyFieldError(field string) error { return InvalidFieldError(field, "must not be empty") } + +// ErrForbidding is returned when an operation is attempted which is forbidden +// for the identified caller. +type ErrForbidden string + +// Error() returns the underlying string of the error. +func (e ErrForbidden) Error() string { + return string(e) +} + +// StringError is any error that also happens to have an underlying type of string. +type StringError interface { + error + ~string +} diff --git a/internal/server/evaluator.go b/internal/server/evaluator.go index 0959c85a37..ea3e41d929 100644 --- a/internal/server/evaluator.go +++ b/internal/server/evaluator.go @@ -130,7 +130,7 @@ func (s *Server) evaluate(ctx context.Context, r *flipt.EvaluationRequest) (resp for _, rule := range rules { if rule.Rank < lastRank { resp.Reason = flipt.EvaluationReason_ERROR_EVALUATION_REASON - return resp, errs.ErrInvalidf("rule rank: %d detected out of order", rule.Rank) + return resp, errs.NewErrorf[errs.ErrInvalid]("rule rank: %d detected out of order", rule.Rank) } lastRank = rule.Rank @@ -333,13 +333,13 @@ func matchesNumber(c storage.EvaluationConstraint, v string) (bool, error) { n, err := strconv.ParseFloat(v, 64) if err != nil { - return false, errs.ErrInvalidf("parsing number from %q", v) + return false, errs.NewErrorf[errs.ErrInvalid]("parsing number from %q", v) } // TODO: we should consider parsing this at creation time since it doesn't change and it doesnt make sense to allow invalid constraint values value, err := strconv.ParseFloat(c.Value, 64) if err != nil { - return false, errs.ErrInvalidf("parsing number from %q", c.Value) + return false, errs.NewErrorf[errs.ErrInvalid]("parsing number from %q", c.Value) } switch c.Operator { @@ -375,7 +375,7 @@ func matchesBool(c storage.EvaluationConstraint, v string) (bool, error) { value, err := strconv.ParseBool(v) if err != nil { - return false, errs.ErrInvalidf("parsing boolean from %q", v) + return false, errs.NewErrorf[errs.ErrInvalid]("parsing boolean from %q", v) } switch c.Operator { diff --git a/internal/server/evaluator_test.go b/internal/server/evaluator_test.go index 47d58578c4..19913c82d4 100644 --- a/internal/server/evaluator_test.go +++ b/internal/server/evaluator_test.go @@ -84,7 +84,7 @@ func TestBatchEvaluate_FlagNotFoundExcluded(t *testing.T) { } store.On("GetFlag", mock.Anything, "foo").Return(enabledFlag, nil) store.On("GetFlag", mock.Anything, "bar").Return(disabled, nil) - store.On("GetFlag", mock.Anything, "NotFoundFlag").Return(&flipt.Flag{}, errs.ErrNotFoundf("flag %q", "NotFoundFlag")) + store.On("GetFlag", mock.Anything, "NotFoundFlag").Return(&flipt.Flag{}, errs.NewErrorf[errs.ErrNotFound]("flag %q", "NotFoundFlag")) store.On("GetEvaluationRules", mock.Anything, "foo").Return([]*storage.EvaluationRule{}, nil) @@ -132,7 +132,7 @@ func TestBatchEvaluate_FlagNotFound(t *testing.T) { } store.On("GetFlag", mock.Anything, "foo").Return(enabledFlag, nil) store.On("GetFlag", mock.Anything, "bar").Return(disabled, nil) - store.On("GetFlag", mock.Anything, "NotFoundFlag").Return(&flipt.Flag{}, errs.ErrNotFoundf("flag %q", "NotFoundFlag")) + store.On("GetFlag", mock.Anything, "NotFoundFlag").Return(&flipt.Flag{}, errs.NewErrorf[errs.ErrNotFound]("flag %q", "NotFoundFlag")) store.On("GetEvaluationRules", mock.Anything, "foo").Return([]*storage.EvaluationRule{}, nil) @@ -172,7 +172,7 @@ func TestEvaluate_FlagNotFound(t *testing.T) { } ) - store.On("GetFlag", mock.Anything, "foo").Return(&flipt.Flag{}, errs.ErrNotFoundf("flag %q", "foo")) + store.On("GetFlag", mock.Anything, "foo").Return(&flipt.Flag{}, errs.NewErrorf[errs.ErrNotFound]("flag %q", "foo")) resp, err := s.Evaluate(context.TODO(), &flipt.EvaluationRequest{ EntityId: "1", diff --git a/internal/storage/auth/auth.go b/internal/storage/auth/auth.go index 8046ff50ea..c57d142f51 100644 --- a/internal/storage/auth/auth.go +++ b/internal/storage/auth/auth.go @@ -70,7 +70,7 @@ type DeleteAuthenticationsRequest struct { func (d *DeleteAuthenticationsRequest) Valid() error { if d.ID == nil && d.Method == nil && d.ExpiredBefore == nil { - return errors.ErrInvalidf("id, method or expired-before timestamp is required") + return errors.NewErrorf[errors.ErrInvalid]("id, method or expired-before timestamp is required") } return nil diff --git a/internal/storage/auth/memory/store.go b/internal/storage/auth/memory/store.go index 9e58659914..b312d22e7e 100644 --- a/internal/storage/auth/memory/store.go +++ b/internal/storage/auth/memory/store.go @@ -84,7 +84,7 @@ func WithIDGeneratorFunc(fn func() string) Option { // string which can be used to retrieve the Authentication again via GetAuthenticationByClientToken. func (s *Store) CreateAuthentication(_ context.Context, r *auth.CreateAuthenticationRequest) (string, *rpcauth.Authentication, error) { if r.ExpiresAt != nil && !r.ExpiresAt.IsValid() { - return "", nil, errors.ErrInvalidf("invalid expiry time: %v", r.ExpiresAt) + return "", nil, errors.NewErrorf[errors.ErrInvalid]("invalid expiry time: %v", r.ExpiresAt) } var ( @@ -125,7 +125,7 @@ func (s *Store) GetAuthenticationByClientToken(ctx context.Context, clientToken authentication, ok := s.byToken[hashedToken] s.mu.Unlock() if !ok { - return nil, errors.ErrNotFoundf("getting authentication by token") + return nil, errors.NewErrorf[errors.ErrNotFound]("getting authentication by token") } return authentication, nil @@ -138,7 +138,7 @@ func (s *Store) GetAuthenticationByID(ctx context.Context, id string) (*rpcauth. authentication, ok := s.byID[id] s.mu.Unlock() if !ok { - return nil, errors.ErrNotFoundf("getting authentication by token") + return nil, errors.NewErrorf[errors.ErrNotFound]("getting authentication by token") } return authentication, nil diff --git a/internal/storage/sql/common/flag.go b/internal/storage/sql/common/flag.go index c2f97259ff..68c4a671e8 100644 --- a/internal/storage/sql/common/flag.go +++ b/internal/storage/sql/common/flag.go @@ -56,7 +56,7 @@ func (s *Store) GetFlag(ctx context.Context, key string) (*flipt.Flag, error) { if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, errs.ErrNotFoundf("flag %q", key) + return nil, errs.NewErrorf[errs.ErrNotFound]("flag %q", key) } return nil, err @@ -231,7 +231,7 @@ func (s *Store) UpdateFlag(ctx context.Context, r *flipt.UpdateFlagRequest) (*fl } if count != 1 { - return nil, errs.ErrNotFoundf("flag %q", r.Key) + return nil, errs.NewErrorf[errs.ErrNotFound]("flag %q", r.Key) } return s.GetFlag(ctx, r.Key) @@ -311,7 +311,7 @@ func (s *Store) UpdateVariant(ctx context.Context, r *flipt.UpdateVariantRequest } if count != 1 { - return nil, errs.ErrNotFoundf("variant %q", r.Key) + return nil, errs.NewErrorf[errs.ErrNotFound]("variant %q", r.Key) } var ( diff --git a/internal/storage/sql/common/rule.go b/internal/storage/sql/common/rule.go index 25c1ec84e5..a50fe04729 100644 --- a/internal/storage/sql/common/rule.go +++ b/internal/storage/sql/common/rule.go @@ -34,7 +34,7 @@ func (s *Store) GetRule(ctx context.Context, id string) (*flipt.Rule, error) { if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, errs.ErrNotFoundf("rule %q", id) + return nil, errs.NewErrorf[errs.ErrNotFound]("rule %q", id) } return nil, err @@ -208,7 +208,7 @@ func (s *Store) UpdateRule(ctx context.Context, r *flipt.UpdateRuleRequest) (*fl } if count != 1 { - return nil, errs.ErrNotFoundf("rule %q", r.Id) + return nil, errs.NewErrorf[errs.ErrNotFound]("rule %q", r.Id) } return s.GetRule(ctx, r.Id) @@ -355,7 +355,7 @@ func (s *Store) UpdateDistribution(ctx context.Context, r *flipt.UpdateDistribut } if count != 1 { - return nil, errs.ErrNotFoundf("distribution %q", r.Id) + return nil, errs.NewErrorf[errs.ErrNotFound]("distribution %q", r.Id) } var ( diff --git a/internal/storage/sql/common/segment.go b/internal/storage/sql/common/segment.go index a63483534b..1314453a6b 100644 --- a/internal/storage/sql/common/segment.go +++ b/internal/storage/sql/common/segment.go @@ -40,7 +40,7 @@ func (s *Store) GetSegment(ctx context.Context, key string) (*flipt.Segment, err if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, errs.ErrNotFoundf("segment %q", key) + return nil, errs.NewErrorf[errs.ErrNotFound]("segment %q", key) } return nil, err @@ -215,7 +215,7 @@ func (s *Store) UpdateSegment(ctx context.Context, r *flipt.UpdateSegmentRequest } if count != 1 { - return nil, errs.ErrNotFoundf("segment %q", r.Key) + return nil, errs.NewErrorf[errs.ErrNotFound]("segment %q", r.Key) } return s.GetSegment(ctx, r.Key) @@ -297,7 +297,7 @@ func (s *Store) UpdateConstraint(ctx context.Context, r *flipt.UpdateConstraintR } if count != 1 { - return nil, errs.ErrNotFoundf("constraint %q", r.Id) + return nil, errs.NewErrorf[errs.ErrNotFound]("constraint %q", r.Id) } var ( diff --git a/internal/storage/sql/mysql/mysql.go b/internal/storage/sql/mysql/mysql.go index a85bb580b8..b7e1c7b2f9 100644 --- a/internal/storage/sql/mysql/mysql.go +++ b/internal/storage/sql/mysql/mysql.go @@ -45,7 +45,7 @@ func (s *Store) CreateFlag(ctx context.Context, r *flipt.CreateFlagRequest) (*fl var merr *mysql.MySQLError if errors.As(err, &merr) && merr.Number == constraintUniqueErrCode { - return nil, errs.ErrInvalidf("flag %q is not unique", r.Key) + return nil, errs.NewErrorf[errs.ErrInvalid]("flag %q is not unique", r.Key) } return nil, err @@ -63,9 +63,9 @@ func (s *Store) CreateVariant(ctx context.Context, r *flipt.CreateVariantRequest if errors.As(err, &merr) { switch merr.Number { case constraintForeignKeyErrCode: - return nil, errs.ErrNotFoundf("flag %q", r.FlagKey) + return nil, errs.NewErrorf[errs.ErrNotFound]("flag %q", r.FlagKey) case constraintUniqueErrCode: - return nil, errs.ErrInvalidf("variant %q is not unique", r.Key) + return nil, errs.NewErrorf[errs.ErrInvalid]("variant %q is not unique", r.Key) } } @@ -82,7 +82,7 @@ func (s *Store) UpdateVariant(ctx context.Context, r *flipt.UpdateVariantRequest var merr *mysql.MySQLError if errors.As(err, &merr) && merr.Number == constraintUniqueErrCode { - return nil, errs.ErrInvalidf("variant %q is not unique", r.Key) + return nil, errs.NewErrorf[errs.ErrInvalid]("variant %q is not unique", r.Key) } return nil, err @@ -98,7 +98,7 @@ func (s *Store) CreateSegment(ctx context.Context, r *flipt.CreateSegmentRequest var merr *mysql.MySQLError if errors.As(err, &merr) && merr.Number == constraintUniqueErrCode { - return nil, errs.ErrInvalidf("segment %q is not unique", r.Key) + return nil, errs.NewErrorf[errs.ErrInvalid]("segment %q is not unique", r.Key) } return nil, err @@ -114,7 +114,7 @@ func (s *Store) CreateConstraint(ctx context.Context, r *flipt.CreateConstraintR var merr *mysql.MySQLError if errors.As(err, &merr) && merr.Number == constraintForeignKeyErrCode { - return nil, errs.ErrNotFoundf("segment %q", r.SegmentKey) + return nil, errs.NewErrorf[errs.ErrNotFound]("segment %q", r.SegmentKey) } return nil, err @@ -130,7 +130,7 @@ func (s *Store) CreateRule(ctx context.Context, r *flipt.CreateRuleRequest) (*fl var merr *mysql.MySQLError if errors.As(err, &merr) && merr.Number == constraintForeignKeyErrCode { - return nil, errs.ErrNotFoundf("flag %q or segment %q", r.FlagKey, r.SegmentKey) + return nil, errs.NewErrorf[errs.ErrNotFound]("flag %q or segment %q", r.FlagKey, r.SegmentKey) } return nil, err @@ -146,7 +146,7 @@ func (s *Store) CreateDistribution(ctx context.Context, r *flipt.CreateDistribut var merr *mysql.MySQLError if errors.As(err, &merr) && merr.Number == constraintForeignKeyErrCode { - return nil, errs.ErrNotFoundf("rule %q", r.RuleId) + return nil, errs.NewErrorf[errs.ErrNotFound]("rule %q", r.RuleId) } return nil, err diff --git a/internal/storage/sql/postgres/postgres.go b/internal/storage/sql/postgres/postgres.go index 200863ac0a..a097f65448 100644 --- a/internal/storage/sql/postgres/postgres.go +++ b/internal/storage/sql/postgres/postgres.go @@ -45,7 +45,7 @@ func (s *Store) CreateFlag(ctx context.Context, r *flipt.CreateFlagRequest) (*fl var perr *pq.Error if errors.As(err, &perr) && perr.Code.Name() == constraintUniqueErr { - return nil, errs.ErrInvalidf("flag %q is not unique", r.Key) + return nil, errs.NewErrorf[errs.ErrInvalid]("flag %q is not unique", r.Key) } return nil, err @@ -63,9 +63,9 @@ func (s *Store) CreateVariant(ctx context.Context, r *flipt.CreateVariantRequest if errors.As(err, &perr) { switch perr.Code.Name() { case constraintForeignKeyErr: - return nil, errs.ErrNotFoundf("flag %q", r.FlagKey) + return nil, errs.NewErrorf[errs.ErrNotFound]("flag %q", r.FlagKey) case constraintUniqueErr: - return nil, errs.ErrInvalidf("variant %q is not unique", r.Key) + return nil, errs.NewErrorf[errs.ErrInvalid]("variant %q is not unique", r.Key) } } @@ -82,7 +82,7 @@ func (s *Store) UpdateVariant(ctx context.Context, r *flipt.UpdateVariantRequest var perr *pq.Error if errors.As(err, &perr) && perr.Code.Name() == constraintUniqueErr { - return nil, errs.ErrInvalidf("variant %q is not unique", r.Key) + return nil, errs.NewErrorf[errs.ErrInvalid]("variant %q is not unique", r.Key) } return nil, err @@ -98,7 +98,7 @@ func (s *Store) CreateSegment(ctx context.Context, r *flipt.CreateSegmentRequest var perr *pq.Error if errors.As(err, &perr) && perr.Code.Name() == constraintUniqueErr { - return nil, errs.ErrInvalidf("segment %q is not unique", r.Key) + return nil, errs.NewErrorf[errs.ErrInvalid]("segment %q is not unique", r.Key) } return nil, err @@ -114,7 +114,7 @@ func (s *Store) CreateConstraint(ctx context.Context, r *flipt.CreateConstraintR var perr *pq.Error if errors.As(err, &perr) && perr.Code.Name() == constraintForeignKeyErr { - return nil, errs.ErrNotFoundf("segment %q", r.SegmentKey) + return nil, errs.NewErrorf[errs.ErrNotFound]("segment %q", r.SegmentKey) } return nil, err @@ -130,7 +130,7 @@ func (s *Store) CreateRule(ctx context.Context, r *flipt.CreateRuleRequest) (*fl var perr *pq.Error if errors.As(err, &perr) && perr.Code.Name() == constraintForeignKeyErr { - return nil, errs.ErrNotFoundf("flag %q or segment %q", r.FlagKey, r.SegmentKey) + return nil, errs.NewErrorf[errs.ErrNotFound]("flag %q or segment %q", r.FlagKey, r.SegmentKey) } return nil, err @@ -146,7 +146,7 @@ func (s *Store) CreateDistribution(ctx context.Context, r *flipt.CreateDistribut var perr *pq.Error if errors.As(err, &perr) && perr.Code.Name() == constraintForeignKeyErr { - return nil, errs.ErrNotFoundf("rule %q", r.RuleId) + return nil, errs.NewErrorf[errs.ErrNotFound]("rule %q", r.RuleId) } return nil, err diff --git a/internal/storage/sql/sqlite/sqlite.go b/internal/storage/sql/sqlite/sqlite.go index e7dcac4be4..8abe4018c5 100644 --- a/internal/storage/sql/sqlite/sqlite.go +++ b/internal/storage/sql/sqlite/sqlite.go @@ -42,7 +42,7 @@ func (s *Store) CreateFlag(ctx context.Context, r *flipt.CreateFlagRequest) (*fl var serr sqlite3.Error if errors.As(err, &serr) && serr.Code == sqlite3.ErrConstraint { - return nil, errs.ErrInvalidf("flag %q is not unique", r.Key) + return nil, errs.NewErrorf[errs.ErrInvalid]("flag %q is not unique", r.Key) } return nil, err @@ -60,9 +60,9 @@ func (s *Store) CreateVariant(ctx context.Context, r *flipt.CreateVariantRequest if errors.As(err, &serr) { switch serr.ExtendedCode { case sqlite3.ErrConstraintForeignKey: - return nil, errs.ErrNotFoundf("flag %q", r.FlagKey) + return nil, errs.NewErrorf[errs.ErrNotFound]("flag %q", r.FlagKey) case sqlite3.ErrConstraintUnique: - return nil, errs.ErrInvalidf("variant %q is not unique", r.Key) + return nil, errs.NewErrorf[errs.ErrInvalid]("variant %q is not unique", r.Key) } } @@ -79,7 +79,7 @@ func (s *Store) UpdateVariant(ctx context.Context, r *flipt.UpdateVariantRequest var serr sqlite3.Error if errors.As(err, &serr) && serr.Code == sqlite3.ErrConstraint { - return nil, errs.ErrInvalidf("variant %q is not unique", r.Key) + return nil, errs.NewErrorf[errs.ErrInvalid]("variant %q is not unique", r.Key) } return nil, err @@ -95,7 +95,7 @@ func (s *Store) CreateSegment(ctx context.Context, r *flipt.CreateSegmentRequest var serr sqlite3.Error if errors.As(err, &serr) && serr.Code == sqlite3.ErrConstraint { - return nil, errs.ErrInvalidf("segment %q is not unique", r.Key) + return nil, errs.NewErrorf[errs.ErrInvalid]("segment %q is not unique", r.Key) } return nil, err @@ -111,7 +111,7 @@ func (s *Store) CreateConstraint(ctx context.Context, r *flipt.CreateConstraintR var serr sqlite3.Error if errors.As(err, &serr) && serr.Code == sqlite3.ErrConstraint { - return nil, errs.ErrNotFoundf("segment %q", r.SegmentKey) + return nil, errs.NewErrorf[errs.ErrNotFound]("segment %q", r.SegmentKey) } return nil, err @@ -127,7 +127,7 @@ func (s *Store) CreateRule(ctx context.Context, r *flipt.CreateRuleRequest) (*fl var serr sqlite3.Error if errors.As(err, &serr) && serr.Code == sqlite3.ErrConstraint { - return nil, errs.ErrNotFoundf("flag %q or segment %q", r.FlagKey, r.SegmentKey) + return nil, errs.NewErrorf[errs.ErrNotFound]("flag %q or segment %q", r.FlagKey, r.SegmentKey) } return nil, err @@ -143,7 +143,7 @@ func (s *Store) CreateDistribution(ctx context.Context, r *flipt.CreateDistribut var serr sqlite3.Error if errors.As(err, &serr) && serr.Code == sqlite3.ErrConstraint { - return nil, errs.ErrNotFoundf("rule %q", r.RuleId) + return nil, errs.NewErrorf[errs.ErrNotFound]("rule %q", r.RuleId) } return nil, err diff --git a/rpc/flipt/validation.go b/rpc/flipt/validation.go index 595ca6141b..b492e01682 100644 --- a/rpc/flipt/validation.go +++ b/rpc/flipt/validation.go @@ -378,18 +378,18 @@ func (req *CreateConstraintRequest) Validate() error { switch req.Type { case ComparisonType_STRING_COMPARISON_TYPE: if _, ok := StringOperators[operator]; !ok { - return errors.ErrInvalidf("constraint operator %q is not valid for type string", req.Operator) + return errors.NewErrorf[errors.ErrInvalid]("constraint operator %q is not valid for type string", req.Operator) } case ComparisonType_NUMBER_COMPARISON_TYPE: if _, ok := NumberOperators[operator]; !ok { - return errors.ErrInvalidf("constraint operator %q is not valid for type number", req.Operator) + return errors.NewErrorf[errors.ErrInvalid]("constraint operator %q is not valid for type number", req.Operator) } case ComparisonType_BOOLEAN_COMPARISON_TYPE: if _, ok := BooleanOperators[operator]; !ok { - return errors.ErrInvalidf("constraint operator %q is not valid for type boolean", req.Operator) + return errors.NewErrorf[errors.ErrInvalid]("constraint operator %q is not valid for type boolean", req.Operator) } default: - return errors.ErrInvalidf("invalid constraint type: %q", req.Type.String()) + return errors.NewErrorf[errors.ErrInvalid]("invalid constraint type: %q", req.Type.String()) } if req.Value == "" { @@ -424,18 +424,18 @@ func (req *UpdateConstraintRequest) Validate() error { switch req.Type { case ComparisonType_STRING_COMPARISON_TYPE: if _, ok := StringOperators[operator]; !ok { - return errors.ErrInvalidf("constraint operator %q is not valid for type string", req.Operator) + return errors.NewErrorf[errors.ErrInvalid]("constraint operator %q is not valid for type string", req.Operator) } case ComparisonType_NUMBER_COMPARISON_TYPE: if _, ok := NumberOperators[operator]; !ok { - return errors.ErrInvalidf("constraint operator %q is not valid for type number", req.Operator) + return errors.NewErrorf[errors.ErrInvalid]("constraint operator %q is not valid for type number", req.Operator) } case ComparisonType_BOOLEAN_COMPARISON_TYPE: if _, ok := BooleanOperators[operator]; !ok { - return errors.ErrInvalidf("constraint operator %q is not valid for type boolean", req.Operator) + return errors.NewErrorf[errors.ErrInvalid]("constraint operator %q is not valid for type boolean", req.Operator) } default: - return errors.ErrInvalidf("invalid constraint type: %q", req.Type.String()) + return errors.NewErrorf[errors.ErrInvalid]("invalid constraint type: %q", req.Type.String()) } if req.Value == "" { From 3c8cc8d692497701a5c5f46fb5f53757bba68f02 Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Mon, 12 Dec 2022 16:03:32 +0000 Subject: [PATCH 07/12] test(oidc): increase coverage around invalid state parameter on callback --- errors/errors.go | 15 +++++++--- go.mod | 4 +-- internal/server/auth/method/oidc/server.go | 27 ++++++++++++++---- .../server/auth/method/oidc/server_test.go | 24 ++++++++++++++-- internal/server/middleware/grpc/middleware.go | 28 +++++++------------ .../server/middleware/grpc/middleware_test.go | 5 ++++ 6 files changed, 72 insertions(+), 31 deletions(-) diff --git a/errors/errors.go b/errors/errors.go index 42ea816846..3bda746d07 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -11,6 +11,13 @@ func As[E error](err error) (e E, _ bool) { return e, errors.As(err, &e) } +// AsMatch is the same as As but it returns just a boolean to represent +// whether or not the wrapped type matches the type parameter. +func AsMatch[E error](err error) (match bool) { + _, match = As[E](err) + return +} + // New creates a new error with errors.New func New(s string) error { return errors.New(s) @@ -55,12 +62,12 @@ func EmptyFieldError(field string) error { return InvalidFieldError(field, "must not be empty") } -// ErrForbidding is returned when an operation is attempted which is forbidden -// for the identified caller. -type ErrForbidden string +// ErrUnauthenticated is returned when an operation is attempted by an unauthenticated +// client in an authenticated context. +type ErrUnauthenticated string // Error() returns the underlying string of the error. -func (e ErrForbidden) Error() string { +func (e ErrUnauthenticated) Error() string { return string(e) } diff --git a/go.mod b/go.mod index b46b0737a3..636e714496 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( go.opentelemetry.io/otel/trace v1.11.2 go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 - golang.org/x/oauth2 v0.2.0 + golang.org/x/net v0.2.0 golang.org/x/sync v0.1.0 google.golang.org/grpc v1.51.0 google.golang.org/protobuf v1.28.1 @@ -124,7 +124,7 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.8.0 // indirect golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect - golang.org/x/net v0.2.0 // indirect + golang.org/x/oauth2 v0.2.0 // indirect golang.org/x/sys v0.2.0 // indirect golang.org/x/text v0.4.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/internal/server/auth/method/oidc/server.go b/internal/server/auth/method/oidc/server.go index 8e4f43c41c..1cddbc264c 100644 --- a/internal/server/auth/method/oidc/server.go +++ b/internal/server/auth/method/oidc/server.go @@ -26,6 +26,21 @@ const ( // was not configured var errProviderNotFound = errors.ErrNotFound("provider not found") +// Server is the core OIDC server implementation for Flipt. +// It supports two primary operations: +// - AuthorizeURL +// - Callback +// These are two legs of the OIDC/OAuth flow. +// Step 1 is Flipt establishes a URL directed at the delegated authentication service (e.g. Google). +// The URL is configured using the client ID configured for the provided, a state parameter used to +// prevent CSRF attacks and a callback URL directing back to the Callback operation. +// Step 2 the user-agent navigates to the authorizer and establishes authenticity with them. +// Once established they're redirected to the Callback operation with an authenticity code. +// Step 3 the Callback operation uses this "code" and exchanges with the authorization service +// for an ID Token. The validity of the response is checked (signature verified) and then the identity +// details contained in this response are used to create a temporary Flipt client token. +// This client token can be used to access the rest of the Flipt API. +// Given the user-agent is requestin using HTTP the token is instead established as an HTTP cookie. type Server struct { logger *zap.Logger store storageauth.Store @@ -71,8 +86,10 @@ func (s *Server) AuthorizeURL(ctx context.Context, req *auth.AuthorizeURLRequest } // Callback attempts to authenticate a callback request from a delegated authorization service. -// The supplied state metadata compared with the request state and ensured to be equal. -// The provided code is exchanged with the associated provider for an "id_token". +// Given the request includes a "state" parameter then the requests metadata is interrogated +// for the "flipt_client_state" metadata key. +// This entry must exist and the value match the request state. +// The provided code is exchanged with the associated authorization service provider for an "id_token". // We verify the retrieved "id_token" is valid and for our client. // Once verified we extract the users associated email address. // Given all this completes successfully then we established an associated clientToken in @@ -87,16 +104,16 @@ func (s *Server) Callback(ctx context.Context, req *auth.CallbackRequest) (_ *au if req.State != "" { md, ok := metadata.FromIncomingContext(ctx) if !ok { - return nil, errors.New("missing metadata") + return nil, errors.NewErrorf[errors.ErrUnauthenticated]("missing state parameter") } state, ok := md["flipt_client_state"] if !ok || len(state) < 1 { - return nil, errors.New("missing state") + return nil, errors.NewErrorf[errors.ErrUnauthenticated]("missing state parameter") } if req.State != state[0] { - return nil, errors.New("unexpected state") + return nil, errors.NewErrorf[errors.ErrUnauthenticated]("unexpected state parameter") } } diff --git a/internal/server/auth/method/oidc/server_test.go b/internal/server/auth/method/oidc/server_test.go index ad58c3d1d0..81196ea297 100644 --- a/internal/server/auth/method/oidc/server_test.go +++ b/internal/server/auth/method/oidc/server_test.go @@ -187,12 +187,32 @@ func Test_Server(t *testing.T) { location = resp.Header.Get("Location") }) - t.Log("Redirecting to callback URL:", location) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, location, nil) + require.NoError(t, err) - t.Run("Callback", func(t *testing.T) { + t.Run("Callback (missing state)", func(t *testing.T) { + // using the default client which has no state cookie + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("Callback (invalid state)", func(t *testing.T) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, location, nil) require.NoError(t, err) + req.Header.Set("Cookie", "flipt_client_state=abcdef") + // using the default client which has no state cookie + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("Callback", func(t *testing.T) { resp, err := client.Do(req) require.NoError(t, err) defer resp.Body.Close() diff --git a/internal/server/middleware/grpc/middleware.go b/internal/server/middleware/grpc/middleware.go index 0f32444a9e..7cbff235ef 100644 --- a/internal/server/middleware/grpc/middleware.go +++ b/internal/server/middleware/grpc/middleware.go @@ -4,7 +4,6 @@ import ( "context" "crypto/md5" "encoding/json" - "errors" "fmt" "time" @@ -51,25 +50,18 @@ func ErrorUnaryInterceptor(ctx context.Context, req interface{}, _ *grpc.UnarySe return } - var errnf errs.ErrNotFound - if errors.As(err, &errnf) { - err = status.Error(codes.NotFound, err.Error()) - return - } - - var errin errs.ErrInvalid - if errors.As(err, &errin) { - err = status.Error(codes.InvalidArgument, err.Error()) - return - } - - var errv errs.ErrValidation - if errors.As(err, &errv) { - err = status.Error(codes.InvalidArgument, err.Error()) - return + code := codes.Internal + switch { + case errs.AsMatch[errs.ErrNotFound](err): + code = codes.NotFound + case errs.AsMatch[errs.ErrInvalid](err), + errs.AsMatch[errs.ErrValidation](err): + code = codes.InvalidArgument + case errs.AsMatch[errs.ErrUnauthenticated](err): + code = codes.Unauthenticated } - err = status.Error(codes.Internal, err.Error()) + err = status.Error(code, err.Error()) return } diff --git a/internal/server/middleware/grpc/middleware_test.go b/internal/server/middleware/grpc/middleware_test.go index 0c71c0eab4..54045ecab1 100644 --- a/internal/server/middleware/grpc/middleware_test.go +++ b/internal/server/middleware/grpc/middleware_test.go @@ -98,6 +98,11 @@ func TestErrorUnaryInterceptor(t *testing.T) { wantErr: errors.EmptyFieldError("bar"), wantCode: codes.InvalidArgument, }, + { + name: "unauthenticated error", + wantErr: errors.NewErrorf[errors.ErrUnauthenticated]("user %q not found", "foo"), + wantCode: codes.Unauthenticated, + }, { name: "other error", wantErr: errors.New("foo"), From 590cc8194d825eb82e2c4946e69d96534014e545 Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Mon, 12 Dec 2022 16:05:36 +0000 Subject: [PATCH 08/12] chore: remove excess newline --- internal/server/auth/method/oidc/server.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/server/auth/method/oidc/server.go b/internal/server/auth/method/oidc/server.go index 1cddbc264c..4405f8a6bd 100644 --- a/internal/server/auth/method/oidc/server.go +++ b/internal/server/auth/method/oidc/server.go @@ -146,7 +146,6 @@ func (s *Server) Callback(ctx context.Context, req *auth.CallbackRequest) (_ *au }) if err != nil { return nil, err - } return &auth.CallbackResponse{ From 01f0270664b4dea3acdf71bb468b380918c38d34 Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Tue, 13 Dec 2022 17:05:36 +0000 Subject: [PATCH 09/12] fix(oidc): install missing auth middleware --- internal/cmd/auth.go | 7 +++++++ ui/vite.config.js | 1 + 2 files changed, 8 insertions(+) diff --git a/internal/cmd/auth.go b/internal/cmd/auth.go index 30564bda7a..13f4f52770 100644 --- a/internal/cmd/auth.go +++ b/internal/cmd/auth.go @@ -114,6 +114,7 @@ func authenticationHTTPMount( oidcmiddleware := authoidc.NewHTTPMiddleware(cfg.Session) muxOpts = append(muxOpts, + runtime.WithMetadata(authoidc.ForwardCookies), runtime.WithForwardResponseOption(oidcmiddleware.ForwardResponseOption)) middleware = oidcmiddleware.Handler } @@ -130,6 +131,12 @@ func authenticationHTTPMount( } } + if cfg.Methods.OIDC.Enabled { + if err := rpcauth.RegisterAuthenticationMethodOIDCServiceHandler(ctx, mux, conn); err != nil { + return fmt.Errorf("registering auth grpc gateway: %w", err) + } + } + r.Group(func(r chi.Router) { r.Use(middleware) diff --git a/ui/vite.config.js b/ui/vite.config.js index deff733fdf..90a93e872d 100644 --- a/ui/vite.config.js +++ b/ui/vite.config.js @@ -30,6 +30,7 @@ export default defineConfig(({ mode }) => { port: 8081, proxy: { "/api": `http://${host}:${port}`, + "/auth": `http://${host}:${port}`, "/meta": `http://${host}:${port}`, "/docs": `http://${host}:${port}`, }, From 2714a8e3ef840e8bd551d136959a3c080f50558b Mon Sep 17 00:00:00 2001 From: George Date: Wed, 14 Dec 2022 17:53:16 +0000 Subject: [PATCH 10/12] feat(oidc): support generic OIDC providers (#1223) * feat(oidc): support generic OIDC providers * refactor(config): support maps with keys containing underscore * chore: appease the linters * chore(config): update doc string Co-authored-by: Mark Phelps <209477+markphelps@users.noreply.github.com> * chore(config): update bind function doc * feat(oidc): add more standard OIDC claims * chore(oidc): remove redundent return statement Co-authored-by: Mark Phelps <209477+markphelps@users.noreply.github.com> --- internal/config/authentication.go | 27 +- internal/config/config.go | 134 +++++- internal/config/config_test.go | 120 ++++- internal/server/auth/method/oidc/http.go | 7 - internal/server/auth/method/oidc/server.go | 94 ++-- .../server/auth/method/oidc/server_test.go | 9 +- rpc/flipt/auth/auth.pb.go | 427 ++++++++---------- rpc/flipt/auth/auth.pb.gw.go | 20 +- rpc/flipt/auth/auth.proto | 9 +- swagger/auth/auth.swagger.json | 20 +- 10 files changed, 504 insertions(+), 363 deletions(-) diff --git a/internal/config/authentication.go b/internal/config/authentication.go index 6cb3cbdef8..00fc625114 100644 --- a/internal/config/authentication.go +++ b/internal/config/authentication.go @@ -55,7 +55,8 @@ func (c *AuthenticationConfig) setDefaults(v *viper.Viper) []string { method := map[string]any{"enabled": false} // if the method has been enabled then set the defaults // for its cleanup strategy - if v.GetBool(fmt.Sprintf("authentication.methods.%s.enabled", k)) { + prefix := fmt.Sprintf("authentication.methods.%s", k) + if v.GetBool(prefix + ".enabled") { method["cleanup"] = map[string]any{ "interval": time.Hour, "grace_period": 30 * time.Minute, @@ -148,22 +149,18 @@ type AuthenticationMethodTokenConfig struct { // AuthenticationMethodOIDCConfig configures the OIDC authentication method. // This method can be used to establish browser based sessions. type AuthenticationMethodOIDCConfig struct { - Enabled bool `json:"enabled,omitempty" mapstructure:"enabled"` - Providers AuthenticationMethodOIDCProviders `json:"providers,omitempty" mapstructure:"providers"` - Cleanup *AuthenticationCleanupSchedule `json:"cleanup,omitempty" mapstructure:"cleanup"` + Enabled bool `json:"enabled,omitempty" mapstructure:"enabled"` + Providers map[string]AuthenticationMethodOIDCProvider `json:"providers,omitempty" mapstructure:"providers"` + Cleanup *AuthenticationCleanupSchedule `json:"cleanup,omitempty" mapstructure:"cleanup"` } -// AuthenticationMethodOIDCProviders contains a set of providers for the OIDC authentication method. -type AuthenticationMethodOIDCProviders struct { - Google *AuthenticationMethodOIDCProviderGoogle `json:"google,omitempty" mapstructure:"google"` -} - -// AuthenticationOIDCProviderGoogle configures the Google OIDC provider credentials -type AuthenticationMethodOIDCProviderGoogle struct { - IssuerURL string `json:"issuerURL,omitempty" mapstructure:"issuer_url"` - ClientID string `json:"clientID,omitempty" mapstructure:"client_id"` - ClientSecret string `json:"clientSecret,omitempty" mapstructure:"client_secret"` - RedirectAddress string `json:"redirectAddress,omitempty" mapstructure:"redirect_address"` +// AuthenticationOIDCProvider configures provider credentials +type AuthenticationMethodOIDCProvider struct { + IssuerURL string `json:"issuerURL,omitempty" mapstructure:"issuer_url"` + ClientID string `json:"clientID,omitempty" mapstructure:"client_id"` + ClientSecret string `json:"clientSecret,omitempty" mapstructure:"client_secret"` + RedirectAddress string `json:"redirectAddress,omitempty" mapstructure:"redirect_address"` + Scopes []string `json:"scopes,omitempty" mapstructure:"scopes"` } // AuthenticationCleanupSchedule is used to configure a cleanup goroutine. diff --git a/internal/config/config.go b/internal/config/config.go index bb0016ccec..0b7b40a732 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "os" "reflect" "strings" @@ -93,7 +94,8 @@ func (c *Config) prepare(v *viper.Viper) (validators []validator) { // search for all expected env vars since Viper cannot // infer when doing Unmarshal + AutomaticEnv. // see: https://github.com/spf13/viper/issues/761 - bindEnvVars(v, "", val.Type().Field(i)) + structField := val.Type().Field(i) + bindEnvVars(v, getFliptEnvs(), []string{fieldKey(structField)}, structField.Type) field := val.Field(i).Addr().Interface() @@ -115,37 +117,137 @@ func (c *Config) prepare(v *viper.Viper) (validators []validator) { return } -// bindEnvVars descends into the provided struct field binding any expected -// environment variable keys it finds reflecting struct and field tags. -func bindEnvVars(v *viper.Viper, prefix string, field reflect.StructField) { - tag := field.Tag.Get("mapstructure") - if tag == "" { - tag = strings.ToLower(field.Name) +func fieldKey(field reflect.StructField) string { + if tag := field.Tag.Get("mapstructure"); tag != "" { + return tag } - var ( - key = prefix + tag - typ = field.Type - ) + return strings.ToLower(field.Name) +} +type envBinder interface { + MustBindEnv(...string) +} + +// bindEnvVars descends into the provided struct field binding any expected +// environment variable keys it finds reflecting struct and field tags. +func bindEnvVars(v envBinder, env, prefixes []string, typ reflect.Type) { // descend through pointers if typ.Kind() == reflect.Pointer { typ = typ.Elem() } - // descend into struct fields - if typ.Kind() == reflect.Struct { + switch typ.Kind() { + case reflect.Map: + // recurse into bindEnvVars while signifying that the last + // key was unbound using the wildcard "*". + bindEnvVars(v, env, append(prefixes, "*"), typ.Elem()) + + return + case reflect.Struct: for i := 0; i < typ.NumField(); i++ { structField := typ.Field(i) - // key becomes prefix for sub-fields - bindEnvVars(v, key+".", structField) + key := fieldKey(structField) + bind(env, prefixes, key, func(prefixes []string) { + bindEnvVars(v, env, prefixes, structField.Type) + }) + } + + return + } + + bind(env, prefixes, "", func(prefixes []string) { + v.MustBindEnv(strings.Join(prefixes, ".")) + }) +} + +const wildcard = "*" + +func appendIfNotEmpty(s []string, v ...string) []string { + for _, vs := range v { + if vs != "" { + s = append(s, vs) } + } + + return s +} +// bind invokes the supplied function "fn" with each possible set of +// prefixes for the next prefix ("next"). +// If the last prefix is "*" then we must search the current environment +// for matching env vars to obtain the potential keys which populate +// the unbound map keys. +func bind(env, prefixes []string, next string, fn func([]string)) { + // given the previous entry is non-existent or not the wildcard + if len(prefixes) < 1 || prefixes[len(prefixes)-1] != wildcard { + fn(appendIfNotEmpty(prefixes, next)) return } - v.MustBindEnv(key) + // drop the wildcard and derive all the possible keys from + // existing environment variables. + p := make([]string, len(prefixes)-1) + copy(p, prefixes[:len(prefixes)-1]) + + var ( + // makezero linter doesn't take note of subsequent copy + // nolint https://github.com/ashanbrown/makezero/issues/12 + prefix = strings.ToUpper(strings.Join(append(p, ""), "_")) + keys = strippedKeys(env, prefix, strings.ToUpper(next)) + ) + + for _, key := range keys { + fn(appendIfNotEmpty(p, strings.ToLower(key), next)) + } +} + +// strippedKeys returns a set of keys derived from a list of env var keys. +// It starts by filtering and stripping each key with a matching prefix. +// Given a child delimiter string is supplied it also trims the delimeter string +// and any remaining characters after this suffix. +// +// e.g strippedKeys(["A_B_C_D", "A_B_F_D", "A_B_E_D_G"], "A_B", "D") +// returns ["c", "f", "e"] +// +// It's purpose is to extract the parts of env vars which are likely +// keys in an arbitrary map type. +func strippedKeys(envs []string, prefix, delim string) (keys []string) { + for _, env := range envs { + if strings.HasPrefix(env, prefix) { + env = env[len(prefix):] + if env == "" { + continue + } + + if delim == "" { + keys = append(keys, env) + continue + } + + // cut the string on the child key and take the left hand component + if left, _, ok := strings.Cut(env, "_"+delim); ok { + keys = append(keys, left) + } + } + } + return +} + +// getFliptEnvs returns all environment variables which have FLIPT_ +// as a prefix. It also strips this prefix before appending them to the +// resulting set. +func getFliptEnvs() (envs []string) { + const prefix = "FLIPT_" + for _, e := range os.Environ() { + key, _, ok := strings.Cut(e, "=") + if ok && strings.HasPrefix(key, prefix) { + // strip FLIPT_ off env vars for convenience + envs = append(envs, key[len(prefix):]) + } + } + return envs } func (c *Config) ServeHTTP(w http.ResponseWriter, r *http.Request) { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f77f561685..56f9b46f97 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "os" + "reflect" "strings" "testing" "time" @@ -443,8 +444,8 @@ func TestLoad(t *testing.T) { }, OIDC: AuthenticationMethodOIDCConfig{ Enabled: true, - Providers: AuthenticationMethodOIDCProviders{ - Google: &AuthenticationMethodOIDCProviderGoogle{ + Providers: map[string]AuthenticationMethodOIDCProvider{ + "google": { IssuerURL: "http://accounts.google.com", ClientID: "abcdefg", ClientSecret: "bcdefgh", @@ -573,3 +574,118 @@ func getEnvVars(prefix string, v map[any]any) (vals [][2]string) { return } + +type sliceEnvBinder []string + +func (s *sliceEnvBinder) MustBindEnv(v ...string) { + *s = append(*s, v...) +} + +func Test_mustBindEnv(t *testing.T) { + for _, test := range []struct { + name string + // inputs + env []string + typ any + // expected outputs + bound []string + }{ + { + name: "simple struct", + env: []string{}, + typ: struct { + A string `mapstructure:"a"` + B string `mapstructure:"b"` + C string `mapstructure:"c"` + }{}, + bound: []string{"a", "b", "c"}, + }, + { + name: "nested structs with pointers", + env: []string{}, + typ: struct { + A string `mapstructure:"a"` + B struct { + C *struct { + D int + } `mapstructure:"c"` + E []string `mapstructure:"e"` + } `mapstructure:"b"` + }{}, + bound: []string{"a", "b.c.d", "b.e"}, + }, + { + name: "structs with maps and no environment variables", + env: []string{}, + typ: struct { + A struct { + B map[string]string `mapstructure:"b"` + } `mapstructure:"a"` + }{}, + // no environment variable to direct mappings + bound: []string{}, + }, + { + name: "structs with maps with env", + env: []string{"A_B_FOO", "A_B_BAR", "A_B_BAZ"}, + typ: struct { + A struct { + B map[string]string `mapstructure:"b"` + } `mapstructure:"a"` + }{}, + // no environment variable to direct mappings + bound: []string{"a.b.foo", "a.b.bar", "a.b.baz"}, + }, + { + name: "structs with maps of structs (env not specific enough)", + env: []string{"A_B_FOO", "A_B_BAR"}, + typ: struct { + A struct { + B map[string]struct { + C string `mapstructure:"c"` + D struct { + E int `mapstructure:"e"` + } `mapstructure:"d"` + } `mapstructure:"b"` + } `mapstructure:"a"` + }{}, + // no environment variable to direct mappings + bound: []string{}, + }, + { + name: "structs with maps of structs", + env: []string{ + "A_B_FOO_C", + "A_B_FOO_D", + "A_B_BAR_BAZ_C", + "A_B_BAR_BAZ_D", + }, + typ: struct { + A struct { + B map[string]struct { + C string `mapstructure:"c"` + D struct { + E int `mapstructure:"e"` + } `mapstructure:"d"` + } `mapstructure:"b"` + } `mapstructure:"a"` + }{}, + bound: []string{ + "a.b.foo.c", + "a.b.bar_baz.c", + "a.b.foo.d.e", + "a.b.bar_baz.d.e", + }, + }, + } { + test := test + t.Run(test.name, func(t *testing.T) { + binder := sliceEnvBinder{} + + typ := reflect.TypeOf(test.typ) + bindEnvVars(&binder, test.env, []string{}, typ) + + assert.Equal(t, test.bound, []string(binder)) + }) + } +} diff --git a/internal/server/auth/method/oidc/http.go b/internal/server/auth/method/oidc/http.go index a56697f665..a877596fb4 100644 --- a/internal/server/auth/method/oidc/http.go +++ b/internal/server/auth/method/oidc/http.go @@ -5,7 +5,6 @@ import ( "crypto/rand" "encoding/base64" "encoding/json" - "fmt" "net/http" "strings" "time" @@ -97,12 +96,6 @@ func (m Middleware) Handler(next http.Handler) http.Handler { return } - // rewrite URL paths for friendlier URL support - enumName := "OIDC_PROVIDER_" + strings.ToUpper(provider) - if _, ok := auth.OIDCProvider_value[enumName]; ok { - r.URL.Path = fmt.Sprintf("/auth/v1/method/oidc/%s/%s", enumName, method) - } - if method == "authorize" { query := r.URL.Query() // create a random security token and bind it to diff --git a/internal/server/auth/method/oidc/server.go b/internal/server/auth/method/oidc/server.go index 4405f8a6bd..9c0798731a 100644 --- a/internal/server/auth/method/oidc/server.go +++ b/internal/server/auth/method/oidc/server.go @@ -18,8 +18,12 @@ import ( ) const ( - storageMetadataOIDCProviderKey = "io.flipt.auth.oidc.provider" - storageMetadataIDEmailKey = "io.flipt.auth.id.email" + storageMetadataOIDCProviderKey = "io.flipt.auth.oidc.provider" + storageMetadataIDEmailKey = "io.flipt.auth.oidc.email" + storageMetadataIDEmailVerifiedKey = "io.flipt.auth.oidc.email_verified" + storageMetadataIDNameKey = "io.flipt.auth.oidc.name" + storageMetadataIDProfileKey = "io.flipt.auth.oidc.profile" + storageMetadataIDPictureKey = "io.flipt.auth.oidc.picture" ) // errProviderNotFound is returned when a provider is requested which @@ -127,22 +131,21 @@ func (s *Server) Callback(ctx context.Context, req *auth.CallbackRequest) (_ *au return nil, err } - // Extract custom claims - var claims struct { - Email string `json:"email"` - Verified bool `json:"email_verified"` + metadata := map[string]string{ + storageMetadataOIDCProviderKey: req.Provider, } + + // Extract custom claims + var claims claims if err := responseToken.IDToken().Claims(&claims); err != nil { return nil, err } + claims.addToMetadata(metadata) clientToken, a, err := s.store.CreateAuthentication(ctx, &storageauth.CreateAuthenticationRequest{ Method: auth.Method_METHOD_OIDC, ExpiresAt: timestamppb.New(time.Now().UTC().Add(s.config.Session.TokenLifetime)), - Metadata: map[string]string{ - storageMetadataIDEmailKey: claims.Email, - storageMetadataOIDCProviderKey: req.Provider.String(), - }, + Metadata: metadata, }) if err != nil { return nil, err @@ -158,37 +161,31 @@ func callbackURL(host, provider string) string { return host + "/auth/v1/method/oidc/" + provider + "/callback" } -func (s *Server) providerFor(provider auth.OIDCProvider, state string) (*capoidc.Provider, *capoidc.Req, error) { +func (s *Server) providerFor(provider string, state string) (*capoidc.Provider, *capoidc.Req, error) { var ( - providerConfig = s.config.Methods.OIDC.Providers - config *capoidc.Config - callback string - scopes []string + config *capoidc.Config + callback string ) - switch provider { - case auth.OIDCProvider_OIDC_PROVIDER_GOOGLE: - // Create a new provider config - google := providerConfig.Google - - callback = callbackURL(google.RedirectAddress, "google") - scopes = []string{"profile", "email"} - - var err error - config, err = capoidc.NewConfig( - google.IssuerURL, - google.ClientID, - capoidc.ClientSecret(google.ClientSecret), - []capoidc.Alg{oidc.RS256}, - []string{callback}, - ) - if err != nil { - return nil, nil, err - } - default: + pConfig, ok := s.config.Methods.OIDC.Providers[provider] + if !ok { return nil, nil, fmt.Errorf("requested provider %q: %w", provider, errProviderNotFound) } + callback = callbackURL(pConfig.RedirectAddress, provider) + + var err error + config, err = capoidc.NewConfig( + pConfig.IssuerURL, + pConfig.ClientID, + capoidc.ClientSecret(pConfig.ClientSecret), + []capoidc.Alg{oidc.RS256}, + []string{callback}, + ) + if err != nil { + return nil, nil, err + } + p, err := capoidc.NewProvider(config) if err != nil { return nil, nil, err @@ -196,7 +193,7 @@ func (s *Server) providerFor(provider auth.OIDCProvider, state string) (*capoidc req, err := capoidc.NewRequest(2*time.Minute, callback, capoidc.WithState(state), - capoidc.WithScopes(scopes...), + capoidc.WithScopes(pConfig.Scopes...), capoidc.WithNonce("static"), // TODO(georgemac): dropping nonce for now ) if err != nil { @@ -205,3 +202,28 @@ func (s *Server) providerFor(provider auth.OIDCProvider, state string) (*capoidc return p, req, nil } + +type claims struct { + Email *string `json:"email"` + Verified *bool `json:"email_verified"` + Name *string `json:"name"` + Profile *string `json:"profile"` + Picture *string `json:"picture"` +} + +func (c claims) addToMetadata(m map[string]string) { + set := func(key string, s *string) { + if s != nil && *s != "" { + m[key] = *s + } + } + + set(storageMetadataIDEmailKey, c.Email) + set(storageMetadataIDNameKey, c.Name) + set(storageMetadataIDProfileKey, c.Profile) + set(storageMetadataIDPictureKey, c.Picture) + + if c.Verified != nil { + m[storageMetadataIDEmailVerifiedKey] = fmt.Sprintf("%v", *c.Verified) + } +} diff --git a/internal/server/auth/method/oidc/server_test.go b/internal/server/auth/method/oidc/server_test.go index 81196ea297..b4f7f0aad0 100644 --- a/internal/server/auth/method/oidc/server_test.go +++ b/internal/server/auth/method/oidc/server_test.go @@ -103,8 +103,8 @@ func Test_Server(t *testing.T) { Methods: config.AuthenticationMethods{ OIDC: config.AuthenticationMethodOIDCConfig{ Enabled: true, - Providers: config.AuthenticationMethodOIDCProviders{ - Google: &config.AuthenticationMethodOIDCProviderGoogle{ + Providers: map[string]config.AuthenticationMethodOIDCProvider{ + "google": { IssuerURL: tp.Addr(), ClientID: id, ClientSecret: secret, @@ -229,8 +229,9 @@ func Test_Server(t *testing.T) { assert.Empty(t, response.ClientToken) // middleware moves it to cookie assert.Equal(t, auth.Method_METHOD_OIDC, response.Authentication.Method) assert.Equal(t, map[string]string{ - "io.flipt.auth.id.email": "mark@flipt.io", - "io.flipt.auth.oidc.provider": "OIDC_PROVIDER_GOOGLE", + "io.flipt.auth.oidc.provider": "google", + "io.flipt.auth.oidc.email": "mark@flipt.io", + "io.flipt.auth.oidc.name": "Mark Phelps", }, response.Authentication.Metadata) // ensure expiry is set diff --git a/rpc/flipt/auth/auth.pb.go b/rpc/flipt/auth/auth.pb.go index b78df8d840..82943f2ab2 100644 --- a/rpc/flipt/auth/auth.pb.go +++ b/rpc/flipt/auth/auth.pb.go @@ -72,52 +72,6 @@ func (Method) EnumDescriptor() ([]byte, []int) { return file_auth_auth_proto_rawDescGZIP(), []int{0} } -type OIDCProvider int32 - -const ( - OIDCProvider_OIDC_PROVIDER_NONE OIDCProvider = 0 - OIDCProvider_OIDC_PROVIDER_GOOGLE OIDCProvider = 1 -) - -// Enum value maps for OIDCProvider. -var ( - OIDCProvider_name = map[int32]string{ - 0: "OIDC_PROVIDER_NONE", - 1: "OIDC_PROVIDER_GOOGLE", - } - OIDCProvider_value = map[string]int32{ - "OIDC_PROVIDER_NONE": 0, - "OIDC_PROVIDER_GOOGLE": 1, - } -) - -func (x OIDCProvider) Enum() *OIDCProvider { - p := new(OIDCProvider) - *p = x - return p -} - -func (x OIDCProvider) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (OIDCProvider) Descriptor() protoreflect.EnumDescriptor { - return file_auth_auth_proto_enumTypes[1].Descriptor() -} - -func (OIDCProvider) Type() protoreflect.EnumType { - return &file_auth_auth_proto_enumTypes[1] -} - -func (x OIDCProvider) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use OIDCProvider.Descriptor instead. -func (OIDCProvider) EnumDescriptor() ([]byte, []int) { - return file_auth_auth_proto_rawDescGZIP(), []int{1} -} - type Authentication struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -540,8 +494,8 @@ type AuthorizeURLRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Provider OIDCProvider `protobuf:"varint,1,opt,name=provider,proto3,enum=flipt.auth.OIDCProvider" json:"provider,omitempty"` - State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` } func (x *AuthorizeURLRequest) Reset() { @@ -576,11 +530,11 @@ func (*AuthorizeURLRequest) Descriptor() ([]byte, []int) { return file_auth_auth_proto_rawDescGZIP(), []int{7} } -func (x *AuthorizeURLRequest) GetProvider() OIDCProvider { +func (x *AuthorizeURLRequest) GetProvider() string { if x != nil { return x.Provider } - return OIDCProvider_OIDC_PROVIDER_NONE + return "" } func (x *AuthorizeURLRequest) GetState() string { @@ -642,9 +596,9 @@ type CallbackRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Provider OIDCProvider `protobuf:"varint,1,opt,name=provider,proto3,enum=flipt.auth.OIDCProvider" json:"provider,omitempty"` - Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"` - State string `protobuf:"bytes,3,opt,name=state,proto3" json:"state,omitempty"` + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"` + State string `protobuf:"bytes,3,opt,name=state,proto3" json:"state,omitempty"` } func (x *CallbackRequest) Reset() { @@ -679,11 +633,11 @@ func (*CallbackRequest) Descriptor() ([]byte, []int) { return file_auth_auth_proto_rawDescGZIP(), []int{9} } -func (x *CallbackRequest) GetProvider() OIDCProvider { +func (x *CallbackRequest) GetProvider() string { if x != nil { return x.Provider } - return OIDCProvider_OIDC_PROVIDER_NONE + return "" } func (x *CallbackRequest) GetCode() string { @@ -833,148 +787,140 @@ var file_auth_auth_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x22, 0x61, 0x0a, 0x13, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x55, 0x52, 0x4c, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x66, 0x6c, 0x69, 0x70, - 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x4f, 0x49, 0x44, 0x43, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x14, 0x0a, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x22, 0x3b, 0x0a, 0x14, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, - 0x55, 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x61, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0c, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x55, 0x72, 0x6c, - 0x22, 0x71, 0x0a, 0x0f, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, - 0x74, 0x68, 0x2e, 0x4f, 0x49, 0x44, 0x43, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, - 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x22, 0x79, 0x0a, 0x10, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x42, 0x0a, 0x0e, 0x61, 0x75, - 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, - 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, - 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2a, 0x3c, - 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x0f, 0x0a, 0x0b, 0x4d, 0x45, 0x54, 0x48, - 0x4f, 0x44, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x4d, 0x45, 0x54, - 0x48, 0x4f, 0x44, 0x5f, 0x54, 0x4f, 0x4b, 0x45, 0x4e, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4d, - 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x4f, 0x49, 0x44, 0x43, 0x10, 0x02, 0x2a, 0x40, 0x0a, 0x0c, - 0x4f, 0x49, 0x44, 0x43, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x12, - 0x4f, 0x49, 0x44, 0x43, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x4e, 0x4f, - 0x4e, 0x45, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x4f, 0x49, 0x44, 0x43, 0x5f, 0x50, 0x52, 0x4f, - 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x47, 0x4f, 0x4f, 0x47, 0x4c, 0x45, 0x10, 0x01, 0x32, 0xab, - 0x05, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xa0, 0x01, 0x0a, 0x15, 0x47, 0x65, 0x74, - 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, - 0x6c, 0x66, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x66, 0x6c, 0x69, - 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x53, 0x92, 0x41, 0x50, 0x0a, 0x0e, 0x61, 0x75, 0x74, - 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x34, 0x47, 0x65, 0x74, - 0x20, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, - 0x66, 0x6f, 0x72, 0x20, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x20, 0x61, 0x75, 0x74, 0x68, - 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, - 0x74, 0x2a, 0x08, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x65, 0x6c, 0x66, 0x12, 0x98, 0x01, 0x0a, 0x11, - 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x24, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x47, - 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x22, 0x41, 0x92, 0x41, 0x3e, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, - 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x18, 0x47, 0x65, 0x74, 0x20, 0x41, 0x75, - 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x62, 0x79, 0x20, - 0x49, 0x44, 0x2a, 0x12, 0x67, 0x65, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0xb0, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x41, - 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26, - 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x48, 0x92, 0x41, 0x45, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x1d, 0x4c, 0x69, 0x73, 0x74, 0x20, 0x41, 0x75, 0x74, 0x68, 0x65, - 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, - 0x63, 0x65, 0x73, 0x2a, 0x14, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, - 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0xa0, 0x01, 0x0a, 0x14, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x27, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x22, 0x47, 0x92, 0x41, 0x44, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, - 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x1b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x20, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, - 0x62, 0x79, 0x20, 0x49, 0x44, 0x2a, 0x15, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x61, 0x75, - 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0xc9, 0x01, 0x0a, - 0x20, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x12, 0xa4, 0x01, 0x0a, 0x0b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x12, 0x1e, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1f, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x54, 0x92, 0x41, 0x51, 0x0a, 0x2a, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x20, 0x74, 0x6f, - 0x6b, 0x65, 0x6e, 0x1a, 0x1b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x20, 0x61, 0x75, 0x74, 0x68, - 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x2a, 0x06, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x32, 0xec, 0x02, 0x0a, 0x1f, 0x41, 0x75, 0x74, - 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x68, 0x6f, - 0x64, 0x4f, 0x49, 0x44, 0x43, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xad, 0x01, 0x0a, - 0x0c, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x55, 0x52, 0x4c, 0x12, 0x1f, 0x2e, - 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x65, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, + 0x22, 0x47, 0x0a, 0x13, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x55, 0x52, 0x4c, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, 0x3b, 0x0a, 0x14, 0x41, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x5f, 0x75, + 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x65, 0x55, 0x72, 0x6c, 0x22, 0x57, 0x0a, 0x0f, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, + 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x22, + 0x79, 0x0a, 0x10, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x42, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, + 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x5a, 0x92, 0x41, 0x57, 0x0a, 0x29, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x20, 0x6f, 0x69, 0x64, 0x63, - 0x1a, 0x1b, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x20, 0x4f, 0x49, 0x44, 0x43, 0x20, - 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x20, 0x55, 0x52, 0x4c, 0x2a, 0x0d, 0x61, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x12, 0x98, 0x01, 0x0a, - 0x08, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x12, 0x1b, 0x2e, 0x66, 0x6c, 0x69, 0x70, - 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x51, 0x92, 0x41, 0x4e, 0x0a, 0x29, 0x61, 0x75, 0x74, 0x68, 0x65, - 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, - 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x20, - 0x6f, 0x69, 0x64, 0x63, 0x1a, 0x17, 0x4f, 0x49, 0x44, 0x43, 0x20, 0x63, 0x61, 0x6c, 0x6c, 0x62, - 0x61, 0x63, 0x6b, 0x20, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2a, 0x08, 0x63, - 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x42, 0xd4, 0x03, 0x5a, 0x20, 0x67, 0x6f, 0x2e, 0x66, - 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x69, 0x6f, 0x2f, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2f, 0x72, 0x70, - 0x63, 0x2f, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x92, 0x41, 0xae, 0x03, - 0x12, 0xb0, 0x01, 0x0a, 0x19, 0x46, 0x6c, 0x69, 0x70, 0x74, 0x20, 0x41, 0x75, 0x74, 0x68, 0x65, - 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x41, 0x50, 0x49, 0x73, 0x22, 0x3d, - 0x0a, 0x0a, 0x46, 0x6c, 0x69, 0x70, 0x74, 0x20, 0x54, 0x65, 0x61, 0x6d, 0x12, 0x21, 0x68, 0x74, - 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2d, 0x69, 0x6f, 0x2f, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x1a, - 0x0c, 0x64, 0x65, 0x76, 0x40, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x69, 0x6f, 0x2a, 0x4c, 0x0a, - 0x0b, 0x4d, 0x49, 0x54, 0x20, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x68, 0x74, - 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2d, 0x69, 0x6f, 0x2f, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2f, - 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x6d, 0x61, 0x69, 0x6e, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x66, 0x6c, - 0x69, 0x70, 0x74, 0x2f, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x32, 0x06, 0x6c, 0x61, 0x74, - 0x65, 0x73, 0x74, 0x2a, 0x02, 0x01, 0x02, 0x32, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x52, 0x63, 0x0a, 0x03, 0x34, - 0x30, 0x31, 0x12, 0x5c, 0x0a, 0x3d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x20, 0x63, 0x6f, - 0x75, 0x6c, 0x64, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x75, 0x74, 0x68, 0x65, - 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x20, 0x28, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, - 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, - 0x64, 0x29, 0x2e, 0x12, 0x1b, 0x0a, 0x19, 0x1a, 0x17, 0x23, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x72, 0x70, 0x63, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x5a, 0x2a, 0x0a, 0x28, 0x0a, 0x11, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x6f, 0x6b, 0x65, - 0x6e, 0x42, 0x65, 0x61, 0x72, 0x65, 0x72, 0x12, 0x13, 0x08, 0x02, 0x1a, 0x0d, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x02, 0x62, 0x17, 0x0a, 0x15, - 0x0a, 0x11, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x65, 0x61, - 0x72, 0x65, 0x72, 0x12, 0x00, 0x72, 0x27, 0x0a, 0x0a, 0x46, 0x6c, 0x69, 0x70, 0x74, 0x20, 0x44, - 0x6f, 0x63, 0x73, 0x12, 0x19, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, - 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x69, 0x6f, 0x2f, 0x64, 0x6f, 0x63, 0x73, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, + 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2a, 0x3c, 0x0a, 0x06, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x12, 0x0f, 0x0a, 0x0b, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x4e, + 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, + 0x54, 0x4f, 0x4b, 0x45, 0x4e, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4d, 0x45, 0x54, 0x48, 0x4f, + 0x44, 0x5f, 0x4f, 0x49, 0x44, 0x43, 0x10, 0x02, 0x32, 0xab, 0x05, 0x0a, 0x15, 0x41, 0x75, 0x74, + 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0xa0, 0x01, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, + 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x6c, 0x66, 0x12, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1a, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x22, 0x53, 0x92, 0x41, 0x50, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x34, 0x47, 0x65, 0x74, 0x20, 0x41, 0x75, 0x74, 0x68, 0x65, + 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x63, 0x75, + 0x72, 0x72, 0x65, 0x6e, 0x74, 0x20, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x64, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x2a, 0x08, 0x67, 0x65, 0x74, + 0x5f, 0x73, 0x65, 0x6c, 0x66, 0x12, 0x98, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, + 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x2e, 0x66, 0x6c, + 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, + 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1a, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x41, + 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x41, 0x92, + 0x41, 0x3e, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x1a, 0x18, 0x47, 0x65, 0x74, 0x20, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x62, 0x79, 0x20, 0x49, 0x44, 0x2a, 0x12, 0x67, 0x65, + 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0xb0, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, + 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, + 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x27, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x48, 0x92, 0x41, 0x45, 0x0a, 0x0e, + 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x1d, + 0x4c, 0x69, 0x73, 0x74, 0x20, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x20, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x2a, 0x14, 0x6c, + 0x69, 0x73, 0x74, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0xa0, 0x01, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, + 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x2e, 0x66, + 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x47, 0x92, + 0x41, 0x44, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x1a, 0x1b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x65, + 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x62, 0x79, 0x20, 0x49, 0x44, 0x2a, + 0x15, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0xc9, 0x01, 0x0a, 0x20, 0x41, 0x75, 0x74, 0x68, 0x65, + 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xa4, 0x01, 0x0a, 0x0b, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1e, 0x2e, 0x66, 0x6c, + 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x66, 0x6c, + 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x54, 0x92, 0x41, + 0x51, 0x0a, 0x2a, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x20, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x20, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x1a, 0x1b, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x20, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2a, 0x06, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x32, 0xec, 0x02, 0x0a, 0x1f, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x49, 0x44, 0x43, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xad, 0x01, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x65, 0x55, 0x52, 0x4c, 0x12, 0x1f, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x55, 0x52, + 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, + 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x55, + 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5a, 0x92, 0x41, 0x57, 0x0a, + 0x29, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, + 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x20, 0x6f, 0x69, 0x64, 0x63, 0x1a, 0x1b, 0x47, 0x65, 0x6e, 0x65, + 0x72, 0x61, 0x74, 0x65, 0x20, 0x4f, 0x49, 0x44, 0x43, 0x20, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x65, 0x20, 0x55, 0x52, 0x4c, 0x2a, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, + 0x7a, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x12, 0x98, 0x01, 0x0a, 0x08, 0x43, 0x61, 0x6c, 0x6c, 0x62, + 0x61, 0x63, 0x6b, 0x12, 0x1b, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1c, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x43, 0x61, + 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x51, + 0x92, 0x41, 0x4e, 0x0a, 0x29, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x20, 0x6f, 0x69, 0x64, 0x63, 0x1a, 0x17, + 0x4f, 0x49, 0x44, 0x43, 0x20, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x20, 0x6f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2a, 0x08, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, + 0x6b, 0x42, 0xd4, 0x03, 0x5a, 0x20, 0x67, 0x6f, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x69, + 0x6f, 0x2f, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x66, 0x6c, 0x69, 0x70, + 0x74, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x92, 0x41, 0xae, 0x03, 0x12, 0xb0, 0x01, 0x0a, 0x19, 0x46, + 0x6c, 0x69, 0x70, 0x74, 0x20, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x20, 0x41, 0x50, 0x49, 0x73, 0x22, 0x3d, 0x0a, 0x0a, 0x46, 0x6c, 0x69, 0x70, + 0x74, 0x20, 0x54, 0x65, 0x61, 0x6d, 0x12, 0x21, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6c, 0x69, 0x70, 0x74, + 0x2d, 0x69, 0x6f, 0x2f, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x1a, 0x0c, 0x64, 0x65, 0x76, 0x40, 0x66, + 0x6c, 0x69, 0x70, 0x74, 0x2e, 0x69, 0x6f, 0x2a, 0x4c, 0x0a, 0x0b, 0x4d, 0x49, 0x54, 0x20, 0x4c, + 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6c, 0x69, 0x70, 0x74, + 0x2d, 0x69, 0x6f, 0x2f, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x6d, + 0x61, 0x69, 0x6e, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x66, 0x6c, 0x69, 0x70, 0x74, 0x2f, 0x4c, 0x49, + 0x43, 0x45, 0x4e, 0x53, 0x45, 0x32, 0x06, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x2a, 0x02, 0x01, + 0x02, 0x32, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, + 0x73, 0x6f, 0x6e, 0x3a, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x52, 0x63, 0x0a, 0x03, 0x34, 0x30, 0x31, 0x12, 0x5c, 0x0a, 0x3d, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x20, 0x63, 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x6e, 0x6f, + 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x64, 0x20, 0x28, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x29, 0x2e, 0x12, 0x1b, 0x0a, + 0x19, 0x1a, 0x17, 0x23, 0x2f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2f, 0x72, 0x70, 0x63, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5a, 0x2a, 0x0a, 0x28, 0x0a, 0x11, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x65, 0x61, 0x72, 0x65, + 0x72, 0x12, 0x13, 0x08, 0x02, 0x1a, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x02, 0x62, 0x17, 0x0a, 0x15, 0x0a, 0x11, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x42, 0x65, 0x61, 0x72, 0x65, 0x72, 0x12, 0x00, 0x72, + 0x27, 0x0a, 0x0a, 0x46, 0x6c, 0x69, 0x70, 0x74, 0x20, 0x44, 0x6f, 0x63, 0x73, 0x12, 0x19, 0x68, + 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x66, 0x6c, 0x69, 0x70, 0x74, + 0x2e, 0x69, 0x6f, 0x2f, 0x64, 0x6f, 0x63, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -989,58 +935,55 @@ func file_auth_auth_proto_rawDescGZIP() []byte { return file_auth_auth_proto_rawDescData } -var file_auth_auth_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_auth_auth_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_auth_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_auth_auth_proto_goTypes = []interface{}{ (Method)(0), // 0: flipt.auth.Method - (OIDCProvider)(0), // 1: flipt.auth.OIDCProvider - (*Authentication)(nil), // 2: flipt.auth.Authentication - (*GetAuthenticationRequest)(nil), // 3: flipt.auth.GetAuthenticationRequest - (*ListAuthenticationsRequest)(nil), // 4: flipt.auth.ListAuthenticationsRequest - (*ListAuthenticationsResponse)(nil), // 5: flipt.auth.ListAuthenticationsResponse - (*DeleteAuthenticationRequest)(nil), // 6: flipt.auth.DeleteAuthenticationRequest - (*CreateTokenRequest)(nil), // 7: flipt.auth.CreateTokenRequest - (*CreateTokenResponse)(nil), // 8: flipt.auth.CreateTokenResponse - (*AuthorizeURLRequest)(nil), // 9: flipt.auth.AuthorizeURLRequest - (*AuthorizeURLResponse)(nil), // 10: flipt.auth.AuthorizeURLResponse - (*CallbackRequest)(nil), // 11: flipt.auth.CallbackRequest - (*CallbackResponse)(nil), // 12: flipt.auth.CallbackResponse - nil, // 13: flipt.auth.Authentication.MetadataEntry - (*timestamppb.Timestamp)(nil), // 14: google.protobuf.Timestamp - (*emptypb.Empty)(nil), // 15: google.protobuf.Empty + (*Authentication)(nil), // 1: flipt.auth.Authentication + (*GetAuthenticationRequest)(nil), // 2: flipt.auth.GetAuthenticationRequest + (*ListAuthenticationsRequest)(nil), // 3: flipt.auth.ListAuthenticationsRequest + (*ListAuthenticationsResponse)(nil), // 4: flipt.auth.ListAuthenticationsResponse + (*DeleteAuthenticationRequest)(nil), // 5: flipt.auth.DeleteAuthenticationRequest + (*CreateTokenRequest)(nil), // 6: flipt.auth.CreateTokenRequest + (*CreateTokenResponse)(nil), // 7: flipt.auth.CreateTokenResponse + (*AuthorizeURLRequest)(nil), // 8: flipt.auth.AuthorizeURLRequest + (*AuthorizeURLResponse)(nil), // 9: flipt.auth.AuthorizeURLResponse + (*CallbackRequest)(nil), // 10: flipt.auth.CallbackRequest + (*CallbackResponse)(nil), // 11: flipt.auth.CallbackResponse + nil, // 12: flipt.auth.Authentication.MetadataEntry + (*timestamppb.Timestamp)(nil), // 13: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 14: google.protobuf.Empty } var file_auth_auth_proto_depIdxs = []int32{ 0, // 0: flipt.auth.Authentication.method:type_name -> flipt.auth.Method - 14, // 1: flipt.auth.Authentication.expires_at:type_name -> google.protobuf.Timestamp - 14, // 2: flipt.auth.Authentication.created_at:type_name -> google.protobuf.Timestamp - 14, // 3: flipt.auth.Authentication.updated_at:type_name -> google.protobuf.Timestamp - 13, // 4: flipt.auth.Authentication.metadata:type_name -> flipt.auth.Authentication.MetadataEntry + 13, // 1: flipt.auth.Authentication.expires_at:type_name -> google.protobuf.Timestamp + 13, // 2: flipt.auth.Authentication.created_at:type_name -> google.protobuf.Timestamp + 13, // 3: flipt.auth.Authentication.updated_at:type_name -> google.protobuf.Timestamp + 12, // 4: flipt.auth.Authentication.metadata:type_name -> flipt.auth.Authentication.MetadataEntry 0, // 5: flipt.auth.ListAuthenticationsRequest.method:type_name -> flipt.auth.Method - 2, // 6: flipt.auth.ListAuthenticationsResponse.authentications:type_name -> flipt.auth.Authentication - 14, // 7: flipt.auth.CreateTokenRequest.expires_at:type_name -> google.protobuf.Timestamp - 2, // 8: flipt.auth.CreateTokenResponse.authentication:type_name -> flipt.auth.Authentication - 1, // 9: flipt.auth.AuthorizeURLRequest.provider:type_name -> flipt.auth.OIDCProvider - 1, // 10: flipt.auth.CallbackRequest.provider:type_name -> flipt.auth.OIDCProvider - 2, // 11: flipt.auth.CallbackResponse.authentication:type_name -> flipt.auth.Authentication - 15, // 12: flipt.auth.AuthenticationService.GetAuthenticationSelf:input_type -> google.protobuf.Empty - 3, // 13: flipt.auth.AuthenticationService.GetAuthentication:input_type -> flipt.auth.GetAuthenticationRequest - 4, // 14: flipt.auth.AuthenticationService.ListAuthentications:input_type -> flipt.auth.ListAuthenticationsRequest - 6, // 15: flipt.auth.AuthenticationService.DeleteAuthentication:input_type -> flipt.auth.DeleteAuthenticationRequest - 7, // 16: flipt.auth.AuthenticationMethodTokenService.CreateToken:input_type -> flipt.auth.CreateTokenRequest - 9, // 17: flipt.auth.AuthenticationMethodOIDCService.AuthorizeURL:input_type -> flipt.auth.AuthorizeURLRequest - 11, // 18: flipt.auth.AuthenticationMethodOIDCService.Callback:input_type -> flipt.auth.CallbackRequest - 2, // 19: flipt.auth.AuthenticationService.GetAuthenticationSelf:output_type -> flipt.auth.Authentication - 2, // 20: flipt.auth.AuthenticationService.GetAuthentication:output_type -> flipt.auth.Authentication - 5, // 21: flipt.auth.AuthenticationService.ListAuthentications:output_type -> flipt.auth.ListAuthenticationsResponse - 15, // 22: flipt.auth.AuthenticationService.DeleteAuthentication:output_type -> google.protobuf.Empty - 8, // 23: flipt.auth.AuthenticationMethodTokenService.CreateToken:output_type -> flipt.auth.CreateTokenResponse - 10, // 24: flipt.auth.AuthenticationMethodOIDCService.AuthorizeURL:output_type -> flipt.auth.AuthorizeURLResponse - 12, // 25: flipt.auth.AuthenticationMethodOIDCService.Callback:output_type -> flipt.auth.CallbackResponse - 19, // [19:26] is the sub-list for method output_type - 12, // [12:19] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 1, // 6: flipt.auth.ListAuthenticationsResponse.authentications:type_name -> flipt.auth.Authentication + 13, // 7: flipt.auth.CreateTokenRequest.expires_at:type_name -> google.protobuf.Timestamp + 1, // 8: flipt.auth.CreateTokenResponse.authentication:type_name -> flipt.auth.Authentication + 1, // 9: flipt.auth.CallbackResponse.authentication:type_name -> flipt.auth.Authentication + 14, // 10: flipt.auth.AuthenticationService.GetAuthenticationSelf:input_type -> google.protobuf.Empty + 2, // 11: flipt.auth.AuthenticationService.GetAuthentication:input_type -> flipt.auth.GetAuthenticationRequest + 3, // 12: flipt.auth.AuthenticationService.ListAuthentications:input_type -> flipt.auth.ListAuthenticationsRequest + 5, // 13: flipt.auth.AuthenticationService.DeleteAuthentication:input_type -> flipt.auth.DeleteAuthenticationRequest + 6, // 14: flipt.auth.AuthenticationMethodTokenService.CreateToken:input_type -> flipt.auth.CreateTokenRequest + 8, // 15: flipt.auth.AuthenticationMethodOIDCService.AuthorizeURL:input_type -> flipt.auth.AuthorizeURLRequest + 10, // 16: flipt.auth.AuthenticationMethodOIDCService.Callback:input_type -> flipt.auth.CallbackRequest + 1, // 17: flipt.auth.AuthenticationService.GetAuthenticationSelf:output_type -> flipt.auth.Authentication + 1, // 18: flipt.auth.AuthenticationService.GetAuthentication:output_type -> flipt.auth.Authentication + 4, // 19: flipt.auth.AuthenticationService.ListAuthentications:output_type -> flipt.auth.ListAuthenticationsResponse + 14, // 20: flipt.auth.AuthenticationService.DeleteAuthentication:output_type -> google.protobuf.Empty + 7, // 21: flipt.auth.AuthenticationMethodTokenService.CreateToken:output_type -> flipt.auth.CreateTokenResponse + 9, // 22: flipt.auth.AuthenticationMethodOIDCService.AuthorizeURL:output_type -> flipt.auth.AuthorizeURLResponse + 11, // 23: flipt.auth.AuthenticationMethodOIDCService.Callback:output_type -> flipt.auth.CallbackResponse + 17, // [17:24] is the sub-list for method output_type + 10, // [10:17] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_auth_auth_proto_init() } @@ -1187,7 +1130,7 @@ func file_auth_auth_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_auth_auth_proto_rawDesc, - NumEnums: 2, + NumEnums: 1, NumMessages: 12, NumExtensions: 0, NumServices: 3, diff --git a/rpc/flipt/auth/auth.pb.gw.go b/rpc/flipt/auth/auth.pb.gw.go index 3aa126250d..4e925eece7 100644 --- a/rpc/flipt/auth/auth.pb.gw.go +++ b/rpc/flipt/auth/auth.pb.gw.go @@ -234,7 +234,6 @@ func request_AuthenticationMethodOIDCService_AuthorizeURL_0(ctx context.Context, var ( val string - e int32 ok bool err error _ = err @@ -245,13 +244,11 @@ func request_AuthenticationMethodOIDCService_AuthorizeURL_0(ctx context.Context, return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "provider") } - e, err = runtime.Enum(val, OIDCProvider_value) + protoReq.Provider, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "provider", err) } - protoReq.Provider = OIDCProvider(e) - if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -270,7 +267,6 @@ func local_request_AuthenticationMethodOIDCService_AuthorizeURL_0(ctx context.Co var ( val string - e int32 ok bool err error _ = err @@ -281,13 +277,11 @@ func local_request_AuthenticationMethodOIDCService_AuthorizeURL_0(ctx context.Co return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "provider") } - e, err = runtime.Enum(val, OIDCProvider_value) + protoReq.Provider, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "provider", err) } - protoReq.Provider = OIDCProvider(e) - if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -310,7 +304,6 @@ func request_AuthenticationMethodOIDCService_Callback_0(ctx context.Context, mar var ( val string - e int32 ok bool err error _ = err @@ -321,13 +314,11 @@ func request_AuthenticationMethodOIDCService_Callback_0(ctx context.Context, mar return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "provider") } - e, err = runtime.Enum(val, OIDCProvider_value) + protoReq.Provider, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "provider", err) } - protoReq.Provider = OIDCProvider(e) - if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -346,7 +337,6 @@ func local_request_AuthenticationMethodOIDCService_Callback_0(ctx context.Contex var ( val string - e int32 ok bool err error _ = err @@ -357,13 +347,11 @@ func local_request_AuthenticationMethodOIDCService_Callback_0(ctx context.Contex return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "provider") } - e, err = runtime.Enum(val, OIDCProvider_value) + protoReq.Provider, err = runtime.String(val) if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "provider", err) } - protoReq.Provider = OIDCProvider(e) - if err := req.ParseForm(); err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } diff --git a/rpc/flipt/auth/auth.proto b/rpc/flipt/auth/auth.proto index e6132720a2..864d8f3a43 100644 --- a/rpc/flipt/auth/auth.proto +++ b/rpc/flipt/auth/auth.proto @@ -163,13 +163,8 @@ service AuthenticationMethodTokenService { } } -enum OIDCProvider { - OIDC_PROVIDER_NONE = 0; - OIDC_PROVIDER_GOOGLE = 1; -} - message AuthorizeURLRequest { - OIDCProvider provider = 1; + string provider = 1; string state = 2; } @@ -178,7 +173,7 @@ message AuthorizeURLResponse { } message CallbackRequest { - OIDCProvider provider = 1; + string provider = 1; string code = 2; string state = 3; } diff --git a/swagger/auth/auth.swagger.json b/swagger/auth/auth.swagger.json index 995ce8e076..c94333a4f6 100644 --- a/swagger/auth/auth.swagger.json +++ b/swagger/auth/auth.swagger.json @@ -64,11 +64,7 @@ "name": "provider", "in": "path", "required": true, - "type": "string", - "enum": [ - "OIDC_PROVIDER_NONE", - "OIDC_PROVIDER_GOOGLE" - ] + "type": "string" }, { "name": "state", @@ -111,11 +107,7 @@ "name": "provider", "in": "path", "required": true, - "type": "string", - "enum": [ - "OIDC_PROVIDER_NONE", - "OIDC_PROVIDER_GOOGLE" - ] + "type": "string" }, { "name": "code", @@ -435,14 +427,6 @@ ], "default": "METHOD_NONE" }, - "authOIDCProvider": { - "type": "string", - "enum": [ - "OIDC_PROVIDER_NONE", - "OIDC_PROVIDER_GOOGLE" - ], - "default": "OIDC_PROVIDER_NONE" - }, "protobufAny": { "type": "object", "properties": { From 5f7b17885c0b6516d12a930b22237f0cd32c9c30 Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Wed, 21 Dec 2022 10:12:56 +0000 Subject: [PATCH 11/12] chore(errors): restore Errorf helper functions --- errors/errors.go | 12 ++++++++++++ internal/server/auth/method/oidc/server.go | 6 +++--- internal/server/evaluator.go | 8 ++++---- internal/storage/auth/auth.go | 2 +- internal/storage/sql/common/flag.go | 6 +++--- internal/storage/sql/common/rule.go | 6 +++--- internal/storage/sql/common/segment.go | 6 +++--- internal/storage/sql/mysql/mysql.go | 16 ++++++++-------- internal/storage/sql/postgres/postgres.go | 16 ++++++++-------- internal/storage/sql/sqlite/sqlite.go | 16 ++++++++-------- rpc/flipt/validation.go | 16 ++++++++-------- 11 files changed, 61 insertions(+), 49 deletions(-) diff --git a/errors/errors.go b/errors/errors.go index 9016c9ad1b..3cff8184da 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -31,6 +31,9 @@ func NewErrorf[E StringError](format string, args ...any) error { // ErrNotFound represents a not found error type ErrNotFound string +// ErrNotFoundf is a convience function for producing ErrNotFound. +var ErrNotFoundf = NewErrorf[ErrNotFound] + func (e ErrNotFound) Error() string { return fmt.Sprintf("%s not found", string(e)) } @@ -38,6 +41,9 @@ func (e ErrNotFound) Error() string { // ErrInvalid represents an invalid error type ErrInvalid string +// ErrInvalidf is a convience function for producing ErrInvalid. +var ErrInvalidf = NewErrorf[ErrInvalid] + func (e ErrInvalid) Error() string { return string(e) } @@ -55,6 +61,9 @@ func (e ErrValidation) Error() string { // ErrCanceled is returned when an operation has been prematurely canceled by the requester. type ErrCanceled string +// ErrCanceledf is a convience function for producing ErrCanceled. +var ErrCanceledf = NewErrorf[ErrCanceled] + func (e ErrCanceled) Error() string { return string(e) } @@ -73,6 +82,9 @@ func EmptyFieldError(field string) error { // client in an authenticated context. type ErrUnauthenticated string +// ErrUnauthenticatedf is a convience function for producing ErrUnauthenticated. +var ErrUnauthenticatedf = NewErrorf[ErrUnauthenticated] + // Error() returns the underlying string of the error. func (e ErrUnauthenticated) Error() string { return string(e) diff --git a/internal/server/auth/method/oidc/server.go b/internal/server/auth/method/oidc/server.go index 9c0798731a..558e163852 100644 --- a/internal/server/auth/method/oidc/server.go +++ b/internal/server/auth/method/oidc/server.go @@ -108,16 +108,16 @@ func (s *Server) Callback(ctx context.Context, req *auth.CallbackRequest) (_ *au if req.State != "" { md, ok := metadata.FromIncomingContext(ctx) if !ok { - return nil, errors.NewErrorf[errors.ErrUnauthenticated]("missing state parameter") + return nil, errors.ErrUnauthenticatedf("missing state parameter") } state, ok := md["flipt_client_state"] if !ok || len(state) < 1 { - return nil, errors.NewErrorf[errors.ErrUnauthenticated]("missing state parameter") + return nil, errors.ErrUnauthenticatedf("missing state parameter") } if req.State != state[0] { - return nil, errors.NewErrorf[errors.ErrUnauthenticated]("unexpected state parameter") + return nil, errors.ErrUnauthenticatedf("unexpected state parameter") } } diff --git a/internal/server/evaluator.go b/internal/server/evaluator.go index ea3e41d929..0959c85a37 100644 --- a/internal/server/evaluator.go +++ b/internal/server/evaluator.go @@ -130,7 +130,7 @@ func (s *Server) evaluate(ctx context.Context, r *flipt.EvaluationRequest) (resp for _, rule := range rules { if rule.Rank < lastRank { resp.Reason = flipt.EvaluationReason_ERROR_EVALUATION_REASON - return resp, errs.NewErrorf[errs.ErrInvalid]("rule rank: %d detected out of order", rule.Rank) + return resp, errs.ErrInvalidf("rule rank: %d detected out of order", rule.Rank) } lastRank = rule.Rank @@ -333,13 +333,13 @@ func matchesNumber(c storage.EvaluationConstraint, v string) (bool, error) { n, err := strconv.ParseFloat(v, 64) if err != nil { - return false, errs.NewErrorf[errs.ErrInvalid]("parsing number from %q", v) + return false, errs.ErrInvalidf("parsing number from %q", v) } // TODO: we should consider parsing this at creation time since it doesn't change and it doesnt make sense to allow invalid constraint values value, err := strconv.ParseFloat(c.Value, 64) if err != nil { - return false, errs.NewErrorf[errs.ErrInvalid]("parsing number from %q", c.Value) + return false, errs.ErrInvalidf("parsing number from %q", c.Value) } switch c.Operator { @@ -375,7 +375,7 @@ func matchesBool(c storage.EvaluationConstraint, v string) (bool, error) { value, err := strconv.ParseBool(v) if err != nil { - return false, errs.NewErrorf[errs.ErrInvalid]("parsing boolean from %q", v) + return false, errs.ErrInvalidf("parsing boolean from %q", v) } switch c.Operator { diff --git a/internal/storage/auth/auth.go b/internal/storage/auth/auth.go index c57d142f51..8046ff50ea 100644 --- a/internal/storage/auth/auth.go +++ b/internal/storage/auth/auth.go @@ -70,7 +70,7 @@ type DeleteAuthenticationsRequest struct { func (d *DeleteAuthenticationsRequest) Valid() error { if d.ID == nil && d.Method == nil && d.ExpiredBefore == nil { - return errors.NewErrorf[errors.ErrInvalid]("id, method or expired-before timestamp is required") + return errors.ErrInvalidf("id, method or expired-before timestamp is required") } return nil diff --git a/internal/storage/sql/common/flag.go b/internal/storage/sql/common/flag.go index 68c4a671e8..c2f97259ff 100644 --- a/internal/storage/sql/common/flag.go +++ b/internal/storage/sql/common/flag.go @@ -56,7 +56,7 @@ func (s *Store) GetFlag(ctx context.Context, key string) (*flipt.Flag, error) { if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, errs.NewErrorf[errs.ErrNotFound]("flag %q", key) + return nil, errs.ErrNotFoundf("flag %q", key) } return nil, err @@ -231,7 +231,7 @@ func (s *Store) UpdateFlag(ctx context.Context, r *flipt.UpdateFlagRequest) (*fl } if count != 1 { - return nil, errs.NewErrorf[errs.ErrNotFound]("flag %q", r.Key) + return nil, errs.ErrNotFoundf("flag %q", r.Key) } return s.GetFlag(ctx, r.Key) @@ -311,7 +311,7 @@ func (s *Store) UpdateVariant(ctx context.Context, r *flipt.UpdateVariantRequest } if count != 1 { - return nil, errs.NewErrorf[errs.ErrNotFound]("variant %q", r.Key) + return nil, errs.ErrNotFoundf("variant %q", r.Key) } var ( diff --git a/internal/storage/sql/common/rule.go b/internal/storage/sql/common/rule.go index a50fe04729..25c1ec84e5 100644 --- a/internal/storage/sql/common/rule.go +++ b/internal/storage/sql/common/rule.go @@ -34,7 +34,7 @@ func (s *Store) GetRule(ctx context.Context, id string) (*flipt.Rule, error) { if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, errs.NewErrorf[errs.ErrNotFound]("rule %q", id) + return nil, errs.ErrNotFoundf("rule %q", id) } return nil, err @@ -208,7 +208,7 @@ func (s *Store) UpdateRule(ctx context.Context, r *flipt.UpdateRuleRequest) (*fl } if count != 1 { - return nil, errs.NewErrorf[errs.ErrNotFound]("rule %q", r.Id) + return nil, errs.ErrNotFoundf("rule %q", r.Id) } return s.GetRule(ctx, r.Id) @@ -355,7 +355,7 @@ func (s *Store) UpdateDistribution(ctx context.Context, r *flipt.UpdateDistribut } if count != 1 { - return nil, errs.NewErrorf[errs.ErrNotFound]("distribution %q", r.Id) + return nil, errs.ErrNotFoundf("distribution %q", r.Id) } var ( diff --git a/internal/storage/sql/common/segment.go b/internal/storage/sql/common/segment.go index 1314453a6b..a63483534b 100644 --- a/internal/storage/sql/common/segment.go +++ b/internal/storage/sql/common/segment.go @@ -40,7 +40,7 @@ func (s *Store) GetSegment(ctx context.Context, key string) (*flipt.Segment, err if err != nil { if errors.Is(err, sql.ErrNoRows) { - return nil, errs.NewErrorf[errs.ErrNotFound]("segment %q", key) + return nil, errs.ErrNotFoundf("segment %q", key) } return nil, err @@ -215,7 +215,7 @@ func (s *Store) UpdateSegment(ctx context.Context, r *flipt.UpdateSegmentRequest } if count != 1 { - return nil, errs.NewErrorf[errs.ErrNotFound]("segment %q", r.Key) + return nil, errs.ErrNotFoundf("segment %q", r.Key) } return s.GetSegment(ctx, r.Key) @@ -297,7 +297,7 @@ func (s *Store) UpdateConstraint(ctx context.Context, r *flipt.UpdateConstraintR } if count != 1 { - return nil, errs.NewErrorf[errs.ErrNotFound]("constraint %q", r.Id) + return nil, errs.ErrNotFoundf("constraint %q", r.Id) } var ( diff --git a/internal/storage/sql/mysql/mysql.go b/internal/storage/sql/mysql/mysql.go index b7e1c7b2f9..a85bb580b8 100644 --- a/internal/storage/sql/mysql/mysql.go +++ b/internal/storage/sql/mysql/mysql.go @@ -45,7 +45,7 @@ func (s *Store) CreateFlag(ctx context.Context, r *flipt.CreateFlagRequest) (*fl var merr *mysql.MySQLError if errors.As(err, &merr) && merr.Number == constraintUniqueErrCode { - return nil, errs.NewErrorf[errs.ErrInvalid]("flag %q is not unique", r.Key) + return nil, errs.ErrInvalidf("flag %q is not unique", r.Key) } return nil, err @@ -63,9 +63,9 @@ func (s *Store) CreateVariant(ctx context.Context, r *flipt.CreateVariantRequest if errors.As(err, &merr) { switch merr.Number { case constraintForeignKeyErrCode: - return nil, errs.NewErrorf[errs.ErrNotFound]("flag %q", r.FlagKey) + return nil, errs.ErrNotFoundf("flag %q", r.FlagKey) case constraintUniqueErrCode: - return nil, errs.NewErrorf[errs.ErrInvalid]("variant %q is not unique", r.Key) + return nil, errs.ErrInvalidf("variant %q is not unique", r.Key) } } @@ -82,7 +82,7 @@ func (s *Store) UpdateVariant(ctx context.Context, r *flipt.UpdateVariantRequest var merr *mysql.MySQLError if errors.As(err, &merr) && merr.Number == constraintUniqueErrCode { - return nil, errs.NewErrorf[errs.ErrInvalid]("variant %q is not unique", r.Key) + return nil, errs.ErrInvalidf("variant %q is not unique", r.Key) } return nil, err @@ -98,7 +98,7 @@ func (s *Store) CreateSegment(ctx context.Context, r *flipt.CreateSegmentRequest var merr *mysql.MySQLError if errors.As(err, &merr) && merr.Number == constraintUniqueErrCode { - return nil, errs.NewErrorf[errs.ErrInvalid]("segment %q is not unique", r.Key) + return nil, errs.ErrInvalidf("segment %q is not unique", r.Key) } return nil, err @@ -114,7 +114,7 @@ func (s *Store) CreateConstraint(ctx context.Context, r *flipt.CreateConstraintR var merr *mysql.MySQLError if errors.As(err, &merr) && merr.Number == constraintForeignKeyErrCode { - return nil, errs.NewErrorf[errs.ErrNotFound]("segment %q", r.SegmentKey) + return nil, errs.ErrNotFoundf("segment %q", r.SegmentKey) } return nil, err @@ -130,7 +130,7 @@ func (s *Store) CreateRule(ctx context.Context, r *flipt.CreateRuleRequest) (*fl var merr *mysql.MySQLError if errors.As(err, &merr) && merr.Number == constraintForeignKeyErrCode { - return nil, errs.NewErrorf[errs.ErrNotFound]("flag %q or segment %q", r.FlagKey, r.SegmentKey) + return nil, errs.ErrNotFoundf("flag %q or segment %q", r.FlagKey, r.SegmentKey) } return nil, err @@ -146,7 +146,7 @@ func (s *Store) CreateDistribution(ctx context.Context, r *flipt.CreateDistribut var merr *mysql.MySQLError if errors.As(err, &merr) && merr.Number == constraintForeignKeyErrCode { - return nil, errs.NewErrorf[errs.ErrNotFound]("rule %q", r.RuleId) + return nil, errs.ErrNotFoundf("rule %q", r.RuleId) } return nil, err diff --git a/internal/storage/sql/postgres/postgres.go b/internal/storage/sql/postgres/postgres.go index a097f65448..200863ac0a 100644 --- a/internal/storage/sql/postgres/postgres.go +++ b/internal/storage/sql/postgres/postgres.go @@ -45,7 +45,7 @@ func (s *Store) CreateFlag(ctx context.Context, r *flipt.CreateFlagRequest) (*fl var perr *pq.Error if errors.As(err, &perr) && perr.Code.Name() == constraintUniqueErr { - return nil, errs.NewErrorf[errs.ErrInvalid]("flag %q is not unique", r.Key) + return nil, errs.ErrInvalidf("flag %q is not unique", r.Key) } return nil, err @@ -63,9 +63,9 @@ func (s *Store) CreateVariant(ctx context.Context, r *flipt.CreateVariantRequest if errors.As(err, &perr) { switch perr.Code.Name() { case constraintForeignKeyErr: - return nil, errs.NewErrorf[errs.ErrNotFound]("flag %q", r.FlagKey) + return nil, errs.ErrNotFoundf("flag %q", r.FlagKey) case constraintUniqueErr: - return nil, errs.NewErrorf[errs.ErrInvalid]("variant %q is not unique", r.Key) + return nil, errs.ErrInvalidf("variant %q is not unique", r.Key) } } @@ -82,7 +82,7 @@ func (s *Store) UpdateVariant(ctx context.Context, r *flipt.UpdateVariantRequest var perr *pq.Error if errors.As(err, &perr) && perr.Code.Name() == constraintUniqueErr { - return nil, errs.NewErrorf[errs.ErrInvalid]("variant %q is not unique", r.Key) + return nil, errs.ErrInvalidf("variant %q is not unique", r.Key) } return nil, err @@ -98,7 +98,7 @@ func (s *Store) CreateSegment(ctx context.Context, r *flipt.CreateSegmentRequest var perr *pq.Error if errors.As(err, &perr) && perr.Code.Name() == constraintUniqueErr { - return nil, errs.NewErrorf[errs.ErrInvalid]("segment %q is not unique", r.Key) + return nil, errs.ErrInvalidf("segment %q is not unique", r.Key) } return nil, err @@ -114,7 +114,7 @@ func (s *Store) CreateConstraint(ctx context.Context, r *flipt.CreateConstraintR var perr *pq.Error if errors.As(err, &perr) && perr.Code.Name() == constraintForeignKeyErr { - return nil, errs.NewErrorf[errs.ErrNotFound]("segment %q", r.SegmentKey) + return nil, errs.ErrNotFoundf("segment %q", r.SegmentKey) } return nil, err @@ -130,7 +130,7 @@ func (s *Store) CreateRule(ctx context.Context, r *flipt.CreateRuleRequest) (*fl var perr *pq.Error if errors.As(err, &perr) && perr.Code.Name() == constraintForeignKeyErr { - return nil, errs.NewErrorf[errs.ErrNotFound]("flag %q or segment %q", r.FlagKey, r.SegmentKey) + return nil, errs.ErrNotFoundf("flag %q or segment %q", r.FlagKey, r.SegmentKey) } return nil, err @@ -146,7 +146,7 @@ func (s *Store) CreateDistribution(ctx context.Context, r *flipt.CreateDistribut var perr *pq.Error if errors.As(err, &perr) && perr.Code.Name() == constraintForeignKeyErr { - return nil, errs.NewErrorf[errs.ErrNotFound]("rule %q", r.RuleId) + return nil, errs.ErrNotFoundf("rule %q", r.RuleId) } return nil, err diff --git a/internal/storage/sql/sqlite/sqlite.go b/internal/storage/sql/sqlite/sqlite.go index 8abe4018c5..e7dcac4be4 100644 --- a/internal/storage/sql/sqlite/sqlite.go +++ b/internal/storage/sql/sqlite/sqlite.go @@ -42,7 +42,7 @@ func (s *Store) CreateFlag(ctx context.Context, r *flipt.CreateFlagRequest) (*fl var serr sqlite3.Error if errors.As(err, &serr) && serr.Code == sqlite3.ErrConstraint { - return nil, errs.NewErrorf[errs.ErrInvalid]("flag %q is not unique", r.Key) + return nil, errs.ErrInvalidf("flag %q is not unique", r.Key) } return nil, err @@ -60,9 +60,9 @@ func (s *Store) CreateVariant(ctx context.Context, r *flipt.CreateVariantRequest if errors.As(err, &serr) { switch serr.ExtendedCode { case sqlite3.ErrConstraintForeignKey: - return nil, errs.NewErrorf[errs.ErrNotFound]("flag %q", r.FlagKey) + return nil, errs.ErrNotFoundf("flag %q", r.FlagKey) case sqlite3.ErrConstraintUnique: - return nil, errs.NewErrorf[errs.ErrInvalid]("variant %q is not unique", r.Key) + return nil, errs.ErrInvalidf("variant %q is not unique", r.Key) } } @@ -79,7 +79,7 @@ func (s *Store) UpdateVariant(ctx context.Context, r *flipt.UpdateVariantRequest var serr sqlite3.Error if errors.As(err, &serr) && serr.Code == sqlite3.ErrConstraint { - return nil, errs.NewErrorf[errs.ErrInvalid]("variant %q is not unique", r.Key) + return nil, errs.ErrInvalidf("variant %q is not unique", r.Key) } return nil, err @@ -95,7 +95,7 @@ func (s *Store) CreateSegment(ctx context.Context, r *flipt.CreateSegmentRequest var serr sqlite3.Error if errors.As(err, &serr) && serr.Code == sqlite3.ErrConstraint { - return nil, errs.NewErrorf[errs.ErrInvalid]("segment %q is not unique", r.Key) + return nil, errs.ErrInvalidf("segment %q is not unique", r.Key) } return nil, err @@ -111,7 +111,7 @@ func (s *Store) CreateConstraint(ctx context.Context, r *flipt.CreateConstraintR var serr sqlite3.Error if errors.As(err, &serr) && serr.Code == sqlite3.ErrConstraint { - return nil, errs.NewErrorf[errs.ErrNotFound]("segment %q", r.SegmentKey) + return nil, errs.ErrNotFoundf("segment %q", r.SegmentKey) } return nil, err @@ -127,7 +127,7 @@ func (s *Store) CreateRule(ctx context.Context, r *flipt.CreateRuleRequest) (*fl var serr sqlite3.Error if errors.As(err, &serr) && serr.Code == sqlite3.ErrConstraint { - return nil, errs.NewErrorf[errs.ErrNotFound]("flag %q or segment %q", r.FlagKey, r.SegmentKey) + return nil, errs.ErrNotFoundf("flag %q or segment %q", r.FlagKey, r.SegmentKey) } return nil, err @@ -143,7 +143,7 @@ func (s *Store) CreateDistribution(ctx context.Context, r *flipt.CreateDistribut var serr sqlite3.Error if errors.As(err, &serr) && serr.Code == sqlite3.ErrConstraint { - return nil, errs.NewErrorf[errs.ErrNotFound]("rule %q", r.RuleId) + return nil, errs.ErrNotFoundf("rule %q", r.RuleId) } return nil, err diff --git a/rpc/flipt/validation.go b/rpc/flipt/validation.go index b492e01682..595ca6141b 100644 --- a/rpc/flipt/validation.go +++ b/rpc/flipt/validation.go @@ -378,18 +378,18 @@ func (req *CreateConstraintRequest) Validate() error { switch req.Type { case ComparisonType_STRING_COMPARISON_TYPE: if _, ok := StringOperators[operator]; !ok { - return errors.NewErrorf[errors.ErrInvalid]("constraint operator %q is not valid for type string", req.Operator) + return errors.ErrInvalidf("constraint operator %q is not valid for type string", req.Operator) } case ComparisonType_NUMBER_COMPARISON_TYPE: if _, ok := NumberOperators[operator]; !ok { - return errors.NewErrorf[errors.ErrInvalid]("constraint operator %q is not valid for type number", req.Operator) + return errors.ErrInvalidf("constraint operator %q is not valid for type number", req.Operator) } case ComparisonType_BOOLEAN_COMPARISON_TYPE: if _, ok := BooleanOperators[operator]; !ok { - return errors.NewErrorf[errors.ErrInvalid]("constraint operator %q is not valid for type boolean", req.Operator) + return errors.ErrInvalidf("constraint operator %q is not valid for type boolean", req.Operator) } default: - return errors.NewErrorf[errors.ErrInvalid]("invalid constraint type: %q", req.Type.String()) + return errors.ErrInvalidf("invalid constraint type: %q", req.Type.String()) } if req.Value == "" { @@ -424,18 +424,18 @@ func (req *UpdateConstraintRequest) Validate() error { switch req.Type { case ComparisonType_STRING_COMPARISON_TYPE: if _, ok := StringOperators[operator]; !ok { - return errors.NewErrorf[errors.ErrInvalid]("constraint operator %q is not valid for type string", req.Operator) + return errors.ErrInvalidf("constraint operator %q is not valid for type string", req.Operator) } case ComparisonType_NUMBER_COMPARISON_TYPE: if _, ok := NumberOperators[operator]; !ok { - return errors.NewErrorf[errors.ErrInvalid]("constraint operator %q is not valid for type number", req.Operator) + return errors.ErrInvalidf("constraint operator %q is not valid for type number", req.Operator) } case ComparisonType_BOOLEAN_COMPARISON_TYPE: if _, ok := BooleanOperators[operator]; !ok { - return errors.NewErrorf[errors.ErrInvalid]("constraint operator %q is not valid for type boolean", req.Operator) + return errors.ErrInvalidf("constraint operator %q is not valid for type boolean", req.Operator) } default: - return errors.NewErrorf[errors.ErrInvalid]("invalid constraint type: %q", req.Type.String()) + return errors.ErrInvalidf("invalid constraint type: %q", req.Type.String()) } if req.Value == "" { From f3709d59bdf93ba3aa2bc71952514d42829d17f5 Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Wed, 21 Dec 2022 10:24:40 +0000 Subject: [PATCH 12/12] chore(errors): switch back to Errorf utility functions --- internal/server/evaluator_test.go | 6 +++--- internal/storage/auth/memory/store.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/server/evaluator_test.go b/internal/server/evaluator_test.go index 19913c82d4..47d58578c4 100644 --- a/internal/server/evaluator_test.go +++ b/internal/server/evaluator_test.go @@ -84,7 +84,7 @@ func TestBatchEvaluate_FlagNotFoundExcluded(t *testing.T) { } store.On("GetFlag", mock.Anything, "foo").Return(enabledFlag, nil) store.On("GetFlag", mock.Anything, "bar").Return(disabled, nil) - store.On("GetFlag", mock.Anything, "NotFoundFlag").Return(&flipt.Flag{}, errs.NewErrorf[errs.ErrNotFound]("flag %q", "NotFoundFlag")) + store.On("GetFlag", mock.Anything, "NotFoundFlag").Return(&flipt.Flag{}, errs.ErrNotFoundf("flag %q", "NotFoundFlag")) store.On("GetEvaluationRules", mock.Anything, "foo").Return([]*storage.EvaluationRule{}, nil) @@ -132,7 +132,7 @@ func TestBatchEvaluate_FlagNotFound(t *testing.T) { } store.On("GetFlag", mock.Anything, "foo").Return(enabledFlag, nil) store.On("GetFlag", mock.Anything, "bar").Return(disabled, nil) - store.On("GetFlag", mock.Anything, "NotFoundFlag").Return(&flipt.Flag{}, errs.NewErrorf[errs.ErrNotFound]("flag %q", "NotFoundFlag")) + store.On("GetFlag", mock.Anything, "NotFoundFlag").Return(&flipt.Flag{}, errs.ErrNotFoundf("flag %q", "NotFoundFlag")) store.On("GetEvaluationRules", mock.Anything, "foo").Return([]*storage.EvaluationRule{}, nil) @@ -172,7 +172,7 @@ func TestEvaluate_FlagNotFound(t *testing.T) { } ) - store.On("GetFlag", mock.Anything, "foo").Return(&flipt.Flag{}, errs.NewErrorf[errs.ErrNotFound]("flag %q", "foo")) + store.On("GetFlag", mock.Anything, "foo").Return(&flipt.Flag{}, errs.ErrNotFoundf("flag %q", "foo")) resp, err := s.Evaluate(context.TODO(), &flipt.EvaluationRequest{ EntityId: "1", diff --git a/internal/storage/auth/memory/store.go b/internal/storage/auth/memory/store.go index b312d22e7e..9e58659914 100644 --- a/internal/storage/auth/memory/store.go +++ b/internal/storage/auth/memory/store.go @@ -84,7 +84,7 @@ func WithIDGeneratorFunc(fn func() string) Option { // string which can be used to retrieve the Authentication again via GetAuthenticationByClientToken. func (s *Store) CreateAuthentication(_ context.Context, r *auth.CreateAuthenticationRequest) (string, *rpcauth.Authentication, error) { if r.ExpiresAt != nil && !r.ExpiresAt.IsValid() { - return "", nil, errors.NewErrorf[errors.ErrInvalid]("invalid expiry time: %v", r.ExpiresAt) + return "", nil, errors.ErrInvalidf("invalid expiry time: %v", r.ExpiresAt) } var ( @@ -125,7 +125,7 @@ func (s *Store) GetAuthenticationByClientToken(ctx context.Context, clientToken authentication, ok := s.byToken[hashedToken] s.mu.Unlock() if !ok { - return nil, errors.NewErrorf[errors.ErrNotFound]("getting authentication by token") + return nil, errors.ErrNotFoundf("getting authentication by token") } return authentication, nil @@ -138,7 +138,7 @@ func (s *Store) GetAuthenticationByID(ctx context.Context, id string) (*rpcauth. authentication, ok := s.byID[id] s.mu.Unlock() if !ok { - return nil, errors.NewErrorf[errors.ErrNotFound]("getting authentication by token") + return nil, errors.ErrNotFoundf("getting authentication by token") } return authentication, nil