From b68d9f3396ac143834d10d1155409c65de3a02f0 Mon Sep 17 00:00:00 2001
From: Alena Petraki <alena.petraki@gmail.com>
Date: Wed, 26 Apr 2023 11:09:53 +0300
Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?=
 =?UTF-8?q?=D0=BD=D1=8B=20=D0=BE=D0=BF=D1=86=D0=B8=D0=B8=20=D0=B4=D0=BB?=
 =?UTF-8?q?=D1=8F=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0?=
 =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=BE?=
 =?UTF-8?q?=D0=B2=20Account,=20Content?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 pkg/account/client.go | 136 +++++++++++++++++++++++-------------------
 pkg/account/config.go |  54 +++++++++++++----
 pkg/content/client.go |  90 +---------------------------
 pkg/content/config.go |  97 ++++++++++++++++++++++++++++++
 pkg/transport/grpc.go |  15 +++++
 pkg/transport/http.go |  35 +++++++++++
 6 files changed, 266 insertions(+), 161 deletions(-)
 create mode 100644 pkg/transport/grpc.go
 create mode 100644 pkg/transport/http.go

diff --git a/pkg/account/client.go b/pkg/account/client.go
index 7604f1da..b8127402 100644
--- a/pkg/account/client.go
+++ b/pkg/account/client.go
@@ -9,6 +9,7 @@ import (
 	"time"
 
 	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
 	serviceMembers "git.perx.ru/perxis/perxis-go/pkg/members/middleware"
 	membersObserverTransport "git.perx.ru/perxis/perxis-go/pkg/members/observer/transport/grpc"
 	membersTransport "git.perx.ru/perxis/perxis-go/pkg/members/transport/grpc"
@@ -33,85 +34,41 @@ const (
 func NewClient(ctx context.Context, addr string, opts ...Option) (*Account, *grpc.ClientConn, error) {
 
 	client := &Account{}
-	dialOpts := make([]grpc.DialOption, 0)
-	config := &config{}
+	c := &config{}
 
 	for _, o := range opts {
-		o(config)
+		o(c)
 
 	}
 
-	if config.logger == nil {
-		config.logger = zap.NewNop()
+	if c.logger == nil {
+		c.logger = zap.NewNop()
 	}
 
-	dialOpts = append(dialOpts, grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
+	c.dialOptions = append(c.dialOptions, grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
 		grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()))
 
-	if config.auth == nil {
-		dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
-	} else {
-		if config.auth.TLS != nil {
-
-			certPool := x509.NewCertPool()
-
-			if !certPool.AppendCertsFromPEM(config.auth.TLS.cacert) {
-				return nil, nil, fmt.Errorf("CA certificate not loaded")
-			}
-
-			clientCert, err := tls.X509KeyPair(config.auth.TLS.cert, config.auth.TLS.key)
-			if err != nil {
-				return nil, nil, err
-
-			}
-
-			tlsConfig := &tls.Config{
-				Certificates: []tls.Certificate{clientCert},
-				RootCAs:      certPool,
-			}
-
-			dialOpts = append(dialOpts,
-				grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
-			)
-		}
-
-		if config.auth.OAuth2 != nil {
-			if config.auth.OAuth2 != nil {
-				// create external grpc client
-				conf := &clientcredentials.Config{
-					TokenURL:       config.auth.OAuth2.tokenURL,
-					ClientID:       config.auth.OAuth2.clientID,
-					ClientSecret:   config.auth.OAuth2.clientSecret,
-					EndpointParams: url.Values{"audience": {config.auth.OAuth2.audience}},
-				}
-				cred := oauth.TokenSource{
-					TokenSource: conf.TokenSource(ctx),
-				}
-				dialOpts = append(dialOpts,
-					grpc.WithPerRPCCredentials(cred),
-					grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})), // обязательно использовать tls для credentials oauth.TokenSource https://github.com/grpc/grpc-go/blob/64031cbfcf4d84c026be93ad7b74b3c290100893/credentials/oauth/oauth.go#L160
-				)
-			}
-		}
-
+	authOptions, err := getAuthDialOpts(ctx, c)
+	if err != nil {
+		return nil, nil, err
 	}
 
-	accountConn, err := grpc.Dial(addr, dialOpts...)
+	accountConn, err := grpc.Dial(addr, append(c.dialOptions, authOptions...)...)
 	if err != nil {
 		return nil, nil, err
 	}
 
-	client.Members = membersTransport.NewGRPCClient(accountConn, "", config.clientOptions...)
-	client.Organizations = organizationsTransport.NewGRPCClient(accountConn, "", config.clientOptions...)
-	client.Users = usersTransport.NewGRPCClient(accountConn, "", config.clientOptions...)
-	client.MembersObserver = membersObserverTransport.NewGRPCClient(accountConn, "", config.clientOptions...)
+	client.Members = membersTransport.NewGRPCClient(accountConn, "", c.clientOptions...)
+	client.Organizations = organizationsTransport.NewGRPCClient(accountConn, "", c.clientOptions...)
+	client.Users = usersTransport.NewGRPCClient(accountConn, "", c.clientOptions...)
+	client.MembersObserver = membersObserverTransport.NewGRPCClient(accountConn, "", c.clientOptions...)
 
-	if !config.noCache {
+	if !c.noCache {
 		client = WithCaching(client, DefaultCacheSize, DefaultCacheTTL)
 	}
 
-	if !config.noLog {
-		client = WithLogging(client, config.logger, config.accessLog)
+	if !c.noLog {
+		client = WithLogging(client, c.logger, c.accessLog)
 	}
 
 	return client, accountConn, nil
@@ -136,3 +93,62 @@ func WithLogging(client *Account, logger *zap.Logger, accessLog bool) *Account {
 
 	return &c
 }
+
+func getAuthDialOpts(ctx context.Context, c *config) (opts []grpc.DialOption, err error) {
+
+	if c.auth == nil {
+		opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
+		return
+	}
+
+	if c.auth.oauth2 != nil {
+		// create external grpc client
+		conf := &clientcredentials.Config{
+			TokenURL:       c.auth.oauth2.tokenURL,
+			ClientID:       c.auth.oauth2.clientID,
+			ClientSecret:   c.auth.oauth2.clientSecret,
+			EndpointParams: url.Values{"audience": {c.auth.oauth2.audience}},
+		}
+
+		// обязательно использовать tls для credentials oauth.TokenSource https://github.com/grpc/grpc-go/blob/64031cbfcf4d84c026be93ad7b74b3c290100893/credentials/oauth/oauth.go#L160
+		if c.auth.insecure {
+			return nil, errors.New("oauth requires tls")
+		}
+		if c.auth.tls == nil {
+			c.auth.tls = &tlsConfig{skipVerify: true}
+		}
+
+		opts = append(opts, grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: conf.TokenSource(ctx)}))
+	}
+
+	if c.auth.tls != nil {
+
+		if c.auth.tls.skipVerify {
+			opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})))
+
+		} else {
+			certPool := x509.NewCertPool()
+			if !certPool.AppendCertsFromPEM(c.auth.tls.cacert) {
+				return nil, fmt.Errorf("CA certificate not loaded")
+			}
+
+			clientCert, err := tls.X509KeyPair(c.auth.tls.cert, c.auth.tls.key)
+			if err != nil {
+				return nil, err
+			}
+
+			opts = append(opts,
+				grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
+					Certificates: []tls.Certificate{clientCert},
+					RootCAs:      certPool,
+				})),
+			)
+		}
+	}
+
+	if c.auth.insecure {
+		opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
+	}
+
+	return
+}
diff --git a/pkg/account/config.go b/pkg/account/config.go
index ca7218b5..fa213360 100644
--- a/pkg/account/config.go
+++ b/pkg/account/config.go
@@ -1,8 +1,9 @@
 package account
 
 import (
-	"github.com/go-kit/kit/transport/grpc"
+	kitgrpc "github.com/go-kit/kit/transport/grpc"
 	"go.uber.org/zap"
+	"google.golang.org/grpc"
 )
 
 type config struct {
@@ -12,27 +13,30 @@ type config struct {
 	accessLog bool
 	debug     bool
 
-	clientOptions []grpc.ClientOption
+	clientOptions []kitgrpc.ClientOption
+	dialOptions   []grpc.DialOption
 
 	logger *zap.Logger
 }
 
 type authConfig struct {
-	OAuth2 *authOAuth2Config
-	TLS    *authTLSConfig
+	oauth2   *oauth2Config
+	tls      *tlsConfig
+	insecure bool
 }
 
-type authOAuth2Config struct {
+type oauth2Config struct {
 	tokenURL     string
 	clientID     string // параметр из auth0 (клиент с таким id должен быть создан в perxis)
 	clientSecret string
 	audience     string // параметр из auth0 (название связанного с Application API)
 }
 
-type authTLSConfig struct {
-	cacert []byte
-	cert   []byte
-	key    []byte
+type tlsConfig struct {
+	cacert     []byte
+	cert       []byte
+	key        []byte
+	skipVerify bool
 }
 
 type Option func(c *config)
@@ -42,7 +46,7 @@ func AuthOAuth2(tokenUrl, clientID, clientSecret, audience string) Option {
 		if c.auth == nil {
 			c.auth = &authConfig{}
 		}
-		c.auth.OAuth2 = &authOAuth2Config{
+		c.auth.oauth2 = &oauth2Config{
 			tokenURL:     tokenUrl,
 			clientID:     clientID,
 			clientSecret: clientSecret,
@@ -56,7 +60,7 @@ func AuthTLS(cacert, cert, key []byte) Option {
 		if c.auth == nil {
 			c.auth = &authConfig{}
 		}
-		c.auth.TLS = &authTLSConfig{
+		c.auth.tls = &tlsConfig{
 			cacert: cacert,
 			cert:   cert,
 			key:    key,
@@ -64,6 +68,26 @@ func AuthTLS(cacert, cert, key []byte) Option {
 	}
 }
 
+func AuthInsecure() Option {
+	return func(c *config) {
+		if c.auth == nil {
+			c.auth = &authConfig{}
+		}
+		c.auth.insecure = true
+	}
+}
+
+func AuthTLSSkipVerify() Option {
+	return func(c *config) {
+		if c.auth == nil {
+			c.auth = &authConfig{}
+		}
+		c.auth.tls = &tlsConfig{
+			skipVerify: true,
+		}
+	}
+}
+
 func NoCache() Option {
 	return func(c *config) {
 		c.noCache = true
@@ -76,12 +100,18 @@ func NoLog() Option {
 	}
 }
 
-func GrpcClientOptions(opts ...grpc.ClientOption) Option {
+func GrpcClientOptions(opts ...kitgrpc.ClientOption) Option {
 	return func(c *config) {
 		c.clientOptions = opts
 	}
 }
 
+func GrpcDialOptionsOptions(opts ...grpc.DialOption) Option {
+	return func(c *config) {
+		c.dialOptions = opts
+	}
+}
+
 func Logger(logger *zap.Logger) Option {
 	return func(c *config) {
 		c.logger = logger
diff --git a/pkg/content/client.go b/pkg/content/client.go
index a6764842..8e642719 100644
--- a/pkg/content/client.go
+++ b/pkg/content/client.go
@@ -2,11 +2,6 @@ package content
 
 import (
 	"context"
-	"crypto/tls"
-	"crypto/x509"
-	"errors"
-	"fmt"
-	"net/url"
 	"time"
 
 	"git.perx.ru/perxis/perxis-go/pkg/cache"
@@ -31,12 +26,7 @@ import (
 	spacesSvc "git.perx.ru/perxis/perxis-go/pkg/spaces/middleware"
 	spacesTransportGrpc "git.perx.ru/perxis/perxis-go/pkg/spaces/transport/grpc"
 	"go.uber.org/zap"
-	"golang.org/x/oauth2/clientcredentials"
 	"google.golang.org/grpc"
-	"google.golang.org/grpc/credentials"
-	"google.golang.org/grpc/credentials/insecure"
-	"google.golang.org/grpc/credentials/oauth"
-	"google.golang.org/grpc/metadata"
 )
 
 const (
@@ -48,7 +38,6 @@ func NewClient(addr string, opts ...Option) (*Content, *grpc.ClientConn, error)
 	ctx := context.Background()
 
 	client := &Content{}
-	dialOpts := make([]grpc.DialOption, 0)
 	config := &Config{}
 
 	for _, o := range opts {
@@ -63,9 +52,8 @@ func NewClient(addr string, opts ...Option) (*Content, *grpc.ClientConn, error)
 	if err != nil {
 		return nil, nil, err
 	}
-	dialOpts = append(dialOpts, authDialOpts...)
 
-	contentConn, err := grpc.Dial(addr, dialOpts...)
+	contentConn, err := grpc.Dial(addr, append(config.DialOptions, authDialOpts...)...)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -129,79 +117,3 @@ func WithLogging(cs *Content, logger *zap.Logger, accessLog bool) *Content {
 
 	return &s
 }
-
-func AddAPIKeyInterceptor(key string) grpc.UnaryClientInterceptor {
-	return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
-		ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "API-Key"+" "+key)
-		return invoker(ctx, method, req, reply, cc, opts...)
-	}
-}
-
-func (c *Config) GetAuthDialOpts(ctx context.Context) (opts []grpc.DialOption, err error) {
-
-	if c.Auth == nil {
-		opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
-		return
-	}
-
-	authConfig := c.Auth
-
-	switch {
-	case authConfig.OAuth2 != nil:
-		// create external grpc client
-		conf := &clientcredentials.Config{
-			TokenURL:       authConfig.OAuth2.TokenURL,
-			ClientID:       authConfig.OAuth2.ClientID,
-			ClientSecret:   authConfig.OAuth2.ClientSecret,
-			EndpointParams: url.Values{"audience": {authConfig.OAuth2.Audience}},
-		}
-
-		// обязательно использовать tls для credentials oauth.TokenSource https://github.com/grpc/grpc-go/blob/64031cbfcf4d84c026be93ad7b74b3c290100893/credentials/oauth/oauth.go#L160
-		if authConfig.Insecure {
-			return nil, errors.New("oauth requires tls")
-		}
-		if authConfig.TLS == nil {
-			authConfig.TLS = &TLS{SkipVerify: true}
-		}
-
-		opts = append(opts,
-			grpc.WithPerRPCCredentials(oauth.TokenSource{
-				TokenSource: conf.TokenSource(ctx),
-			}),
-		)
-	case authConfig.APIKey != nil:
-
-		if !authConfig.Insecure && authConfig.TLS == nil {
-			authConfig.TLS = &TLS{SkipVerify: true}
-		}
-		opts = append(opts,
-			grpc.WithUnaryInterceptor(AddAPIKeyInterceptor(authConfig.APIKey.APIKey)),
-		)
-	}
-
-	switch {
-	case authConfig.TLS != nil && !authConfig.SkipVerify:
-		certPool := x509.NewCertPool()
-		if !certPool.AppendCertsFromPEM(authConfig.TLS.CaCert) {
-			return nil, fmt.Errorf("CA certificate not loaded")
-		}
-
-		clientCert, err := tls.X509KeyPair(authConfig.TLS.Cert, authConfig.TLS.Key)
-		if err != nil {
-			return nil, err
-		}
-
-		opts = append(opts,
-			grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
-				Certificates: []tls.Certificate{clientCert},
-				RootCAs:      certPool,
-			})),
-		)
-	case authConfig.TLS != nil && authConfig.SkipVerify:
-		opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})))
-	default:
-		opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
-	}
-
-	return
-}
diff --git a/pkg/content/config.go b/pkg/content/config.go
index b77d7ddb..e4280505 100644
--- a/pkg/content/config.go
+++ b/pkg/content/config.go
@@ -1,8 +1,21 @@
 package content
 
 import (
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"errors"
+	"fmt"
+	"net/url"
+
+	"git.perx.ru/perxis/perxis-go/pkg/transport"
 	kitgrpc "github.com/go-kit/kit/transport/grpc"
 	"go.uber.org/zap"
+	"golang.org/x/oauth2/clientcredentials"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+	"google.golang.org/grpc/credentials/insecure"
+	"google.golang.org/grpc/credentials/oauth"
 )
 
 type Config struct {
@@ -14,6 +27,7 @@ type Config struct {
 	NoDecode  bool
 
 	ClientOptions []kitgrpc.ClientOption
+	DialOptions   []grpc.DialOption
 
 	Logger *zap.Logger
 }
@@ -72,6 +86,17 @@ func AuthTLS(cacert, cert, key []byte) Option {
 	}
 }
 
+func AuthTLSSkipVerify() Option {
+	return func(c *Config) {
+		if c.Auth == nil {
+			c.Auth = &AuthConfig{}
+		}
+		c.Auth.TLS = &TLS{
+			SkipVerify: true,
+		}
+	}
+}
+
 func AuthAPIKey(key string) Option {
 	return func(c *Config) {
 		if c.Auth == nil {
@@ -125,3 +150,75 @@ func GrpcClientOptions(opts ...kitgrpc.ClientOption) Option {
 		c.ClientOptions = opts
 	}
 }
+
+func GrpcDialOptionsOptions(opts ...grpc.DialOption) Option {
+	return func(c *Config) {
+		c.DialOptions = opts
+	}
+}
+
+func (c *Config) GetAuthDialOpts(ctx context.Context) (opts []grpc.DialOption, err error) {
+
+	if c.Auth == nil {
+		opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
+		return
+	}
+
+	switch {
+	case c.Auth.OAuth2 != nil:
+		// create external grpc client
+		conf := &clientcredentials.Config{
+			TokenURL:       c.Auth.OAuth2.TokenURL,
+			ClientID:       c.Auth.OAuth2.ClientID,
+			ClientSecret:   c.Auth.OAuth2.ClientSecret,
+			EndpointParams: url.Values{"audience": {c.Auth.OAuth2.Audience}},
+		}
+
+		// обязательно использовать tls для credentials oauth.TokenSource https://github.com/grpc/grpc-go/blob/64031cbfcf4d84c026be93ad7b74b3c290100893/credentials/oauth/oauth.go#L160
+		if c.Auth.Insecure {
+			return nil, errors.New("oauth requires tls")
+		}
+		if c.Auth.TLS == nil {
+			c.Auth.TLS = &TLS{SkipVerify: true}
+		}
+
+		opts = append(opts, grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: conf.TokenSource(ctx)}))
+	case c.Auth.APIKey != nil:
+
+		if !c.Auth.Insecure && c.Auth.TLS == nil {
+			c.Auth.TLS = &TLS{SkipVerify: true}
+		}
+		opts = append(opts, grpc.WithUnaryInterceptor(transport.AddAuthorizationInterceptor("API-Key "+c.Auth.APIKey.APIKey)))
+	}
+
+	if c.Auth.TLS != nil {
+
+		if c.Auth.TLS.SkipVerify {
+			opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})))
+
+		} else {
+			certPool := x509.NewCertPool()
+			if !certPool.AppendCertsFromPEM(c.Auth.TLS.CaCert) {
+				return nil, fmt.Errorf("CA certificate not loaded")
+			}
+
+			clientCert, err := tls.X509KeyPair(c.Auth.TLS.Cert, c.Auth.TLS.Key)
+			if err != nil {
+				return nil, err
+			}
+
+			opts = append(opts,
+				grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
+					Certificates: []tls.Certificate{clientCert},
+					RootCAs:      certPool,
+				})),
+			)
+		}
+	}
+
+	if c.Auth.Insecure {
+		opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
+	}
+
+	return
+}
diff --git a/pkg/transport/grpc.go b/pkg/transport/grpc.go
new file mode 100644
index 00000000..adf54f57
--- /dev/null
+++ b/pkg/transport/grpc.go
@@ -0,0 +1,15 @@
+package transport
+
+import (
+	"context"
+
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/metadata"
+)
+
+func AddAuthorizationInterceptor(auth string) grpc.UnaryClientInterceptor {
+	return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
+		ctx = metadata.AppendToOutgoingContext(ctx, "authorization", auth)
+		return invoker(ctx, method, req, reply, cc, opts...)
+	}
+}
diff --git a/pkg/transport/http.go b/pkg/transport/http.go
new file mode 100644
index 00000000..032c0515
--- /dev/null
+++ b/pkg/transport/http.go
@@ -0,0 +1,35 @@
+package transport
+
+import (
+	"bytes"
+	"io"
+	"net/http"
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	jsoniter "github.com/json-iterator/go"
+)
+
+func DecodeError(r *http.Response) error {
+	if r.StatusCode >= 200 && r.StatusCode < 300 {
+		return nil
+	}
+
+	var buf bytes.Buffer
+	if _, err := io.Copy(&buf, io.LimitReader(r.Body, 1024)); err != nil {
+		return err
+	}
+
+	e := struct {
+		Error error `json:"error"`
+	}{}
+	if err := jsoniter.Unmarshal(buf.Bytes(), e); err != nil {
+		msg := strings.TrimSpace(buf.String())
+		if msg == "" {
+			msg = http.StatusText(r.StatusCode)
+		}
+		return errors.New(msg)
+	}
+
+	return e.Error
+}
-- 
GitLab