Select Git revision
grpc.go 8.26 KiB
package auth
import (
"context"
"crypto/tls"
"crypto/x509"
"net/url"
"connectrpc.com/connect"
"git.perx.ru/perxis/perxis-go/pkg/errors"
kitgrpc "github.com/go-kit/kit/transport/grpc"
"golang.org/x/oauth2/clientcredentials"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/oauth"
"google.golang.org/grpc/metadata"
)
const (
OAuth2IdentityMetadata = "x-perxis-identity"
TLSIdentityMetadata = "x-forwarded-client-cert"
AccessMetadata = "x-perxis-access"
AuthorizationMetadata = "authorization"
)
func GRPCToContext(factory *PrincipalFactory) kitgrpc.ServerRequestFunc {
return func(ctx context.Context, md metadata.MD) context.Context {
if identity := md.Get(TLSIdentityMetadata); len(identity) > 0 {
return WithPrincipal(ctx, factory.Principal(identity[0]))
}
if identity := md.Get(OAuth2IdentityMetadata); len(identity) > 0 {
return WithPrincipal(ctx, factory.Principal(identity[0]))
}
if identity := md.Get(AuthorizationMetadata); len(identity) > 0 {
return WithPrincipal(ctx, factory.Principal(identity[0]))
}
if access := md.Get(AccessMetadata); len(access) > 0 {
return WithPrincipal(ctx, factory.System())
}
return WithPrincipal(ctx, factory.Anonymous())
}
}
func ContextToGRPC() kitgrpc.ClientRequestFunc {
return func(ctx context.Context, md *metadata.MD) context.Context {
p := GetPrincipal(ctx)
switch p := p.(type) {
case *UserPrincipal:
if p.GetIdentity(ctx) != "" {
(*md)[OAuth2IdentityMetadata] = []string{p.GetIdentity(ctx)}
}
case *ClientPrincipal:
if ident := p.GetIdentity(ctx); ident != nil {
switch {
case ident.OAuthClientID != "":
(*md)[OAuth2IdentityMetadata] = []string{ident.OAuthClientID + "@clients"}
case ident.TLSSubject != "":
(*md)[TLSIdentityMetadata] = []string{ident.TLSSubject}
case ident.APIKey != "":
(*md)[AuthorizationMetadata] = []string{"API-Key " + ident.APIKey}
}
}
case *SystemPrincipal:
(*md)[AccessMetadata] = []string{p.GetID(ctx)}
}
return ctx
}
}
// PrincipalServerInterceptor - grpc-интерсептор, который используется для получения данных принципала из grpc-метаданы и добавления в контекст ”. В случае, если
// сервис не использует проверку прав 'Principal' к системе, в параметрах передается пустой объект '&PrincipalFactory{}'
func PrincipalServerInterceptor(factory *PrincipalFactory) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if md, ok := metadata.FromIncomingContext(ctx); ok {
ctx = GRPCToContext(factory)(ctx, md)
}
return handler(ctx, req)
}
}
// PrincipalClientInterceptor - grpc-интерсептор, который используется для получения данных принципала. В случае, если
// сервис не использует проверку прав 'Principal' к системе, в параметрах передается пустой объект '&PrincipalFactory{}'
func PrincipalClientInterceptor() grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
md, ok := metadata.FromOutgoingContext(ctx)
if !ok {
md = metadata.MD{}
}
ctx = metadata.NewOutgoingContext(ContextToGRPC()(ctx, &md), md)
return invoker(ctx, method, req, reply, cc, opts...)
}
}
func AddAccessInterceptor(id 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, AccessMetadata, id)
return invoker(ctx, method, req, reply, cc, opts...)
}
}
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...)
}
}
// AddAPIKeyInterceptor возвращает опции для создания grpc-соединения с передачей API-ключа при каждом запросе
func AddAPIKeyInterceptor(key string) grpc.UnaryClientInterceptor {
return AddAuthorizationInterceptor("API-Key " + key)
}
// WithOAuth2Credentials возвращает опции для создания grpc-соединения с аутентификацией oauth2. Переданный контекст
// будет использован для каждого запроса токена, поэтому он не может быть с таймаутом.
func WithOAuth2Credentials(ctx context.Context, tokenURL, clientID, clientSecret, audience string) grpc.DialOption {
conf := &clientcredentials.Config{
TokenURL: tokenURL,
ClientID: clientID,
ClientSecret: clientSecret,
EndpointParams: url.Values{"audience": {audience}},
}
return grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: conf.TokenSource(ctx)})
}
// WithTLSCredentials возвращает опции для создания grpc-соединения с TLS-сертификатами
func WithTLSCredentials(ctx context.Context, cert, cacert, key []byte) (grpc.DialOption, error) {
creds, err := TLSCredentials(ctx, cert, cacert, key)
if err != nil {
return nil, err
}
return grpc.WithTransportCredentials(creds), nil
}
// TLSCredentials возвращает TransportCredentials для создания grpc-соединения с TLS-сертификатами
func TLSCredentials(ctx context.Context, cert, cacert, key []byte) (credentials.TransportCredentials, error) {
certPool := x509.NewCertPool()
if !certPool.AppendCertsFromPEM(cacert) {
return nil, errors.New("CA certificate not loaded")
}
clientCert, err := tls.X509KeyPair(cert, key)
if err != nil {
return nil, err
}
return credentials.NewTLS(&tls.Config{Certificates: []tls.Certificate{clientCert}, RootCAs: certPool}), nil
}
// PrincipalInterceptorConnect интерсептор для клиента и сервера
// используется для получения данных принципала из запроса и добавления в контекст.
func PrincipalInterceptorConnect(factory *PrincipalFactory) connect.UnaryInterceptorFunc { //nolint:gocognit // example
interceptor := func(next connect.UnaryFunc) connect.UnaryFunc {
return func(
ctx context.Context,
req connect.AnyRequest,
) (connect.AnyResponse, error) {
if req.Spec().IsClient {
p := GetPrincipal(ctx)
switch p := p.(type) {
case *UserPrincipal:
if p.GetIdentity(ctx) != "" {
req.Header().Set(OAuth2IdentityMetadata, p.GetIdentity(ctx))
}
case *ClientPrincipal:
if ident := p.GetIdentity(ctx); ident != nil {
switch {
case ident.OAuthClientID != "":
req.Header().Set(OAuth2IdentityMetadata, ident.OAuthClientID+"@clients")
case ident.TLSSubject != "":
req.Header().Set(TLSIdentityMetadata, ident.TLSSubject)
case ident.APIKey != "":
req.Header().Set(AuthorizationMetadata, "API-Key "+ident.APIKey)
}
}
case *SystemPrincipal:
req.Header().Set(AccessMetadata, p.GetID(ctx))
}
return next(ctx, req)
}
if identity := req.Header().Get(TLSIdentityMetadata); identity != "" {
ctx = WithPrincipal(ctx, factory.Principal(identity))
return next(ctx, req)
}
if identity := req.Header().Get(OAuth2IdentityMetadata); identity != "" {
ctx = WithPrincipal(ctx, factory.Principal(identity))
return next(ctx, req)
}
if identity := req.Header().Get(AuthorizationMetadata); identity != "" {
ctx = WithPrincipal(ctx, factory.Principal(identity))
return next(ctx, req)
}
if access := req.Header().Get(AccessMetadata); access != "" {
ctx = WithPrincipal(ctx, factory.Principal(access))
return next(ctx, req)
}
ctx = WithPrincipal(ctx, factory.Anonymous())
return next(ctx, req)
}
}
return interceptor
}