package auth import ( "context" "crypto/tls" "crypto/x509" "net/url" "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) { 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 grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{Certificates: []tls.Certificate{clientCert}, RootCAs: certPool})), nil }