github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/remotesrv/interceptors.go (about)

     1  // Copyright 2023 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package remotesrv
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  
    21  	"github.com/sirupsen/logrus"
    22  	"google.golang.org/grpc"
    23  	"google.golang.org/grpc/codes"
    24  	"google.golang.org/grpc/status"
    25  )
    26  
    27  type RequestCredentials struct {
    28  	Username string
    29  	Password string
    30  	Address  string
    31  }
    32  
    33  type ServerInterceptor struct {
    34  	Lgr              *logrus.Entry
    35  	AccessController AccessControl
    36  }
    37  
    38  var SUPER_USER_RPC_METHODS = map[string]bool{
    39  	"/dolt.services.remotesapi.v1alpha1.ChunkStoreService/AddTableFiles":      true,
    40  	"/dolt.services.remotesapi.v1alpha1.ChunkStoreService/Commit":             true,
    41  	"/dolt.services.remotesapi.v1alpha1.ChunkStoreService/GetUploadLocations": true,
    42  }
    43  
    44  var CLONE_ADMIN_RPC_METHODS = map[string]bool{
    45  	"/dolt.services.remotesapi.v1alpha1.ChunkStoreService/GetDownloadLocations":    true,
    46  	"/dolt.services.remotesapi.v1alpha1.ChunkStoreService/GetRepoMetadata":         true,
    47  	"/dolt.services.remotesapi.v1alpha1.ChunkStoreService/HasChunks":               true,
    48  	"/dolt.services.remotesapi.v1alpha1.ChunkStoreService/ListTableFiles":          true,
    49  	"/dolt.services.remotesapi.v1alpha1.ChunkStoreService/RefreshTableFileUrl":     true,
    50  	"/dolt.services.remotesapi.v1alpha1.ChunkStoreService/Root":                    true,
    51  	"/dolt.services.remotesapi.v1alpha1.ChunkStoreService/StreamDownloadLocations": true,
    52  }
    53  
    54  // AccessControl is an interface that provides authentication and authorization for the gRPC server.
    55  type AccessControl interface {
    56  	// ApiAuthenticate checks the incoming request for authentication credentials and validates them. If the user's
    57  	// identity checks out, the returned context will have the sqlContext within it, which contains the user's ID.
    58  	// If the user is not legitimate, an error is returned.
    59  	ApiAuthenticate(ctx context.Context) (context.Context, error)
    60  	// ApiAuthorize checks that the authenticated user has sufficient privileges to perform the requested action.
    61  	// Currently, our resource policy is binary currently, a user either is a SuperUser (form Commit) or they have a
    62  	// CLONE_ADMIN grant for read operations.
    63  	// More resource aware authorization decisions will be needed in the future, but this is sufficient for now.
    64  	ApiAuthorize(ctx context.Context, superUserReq bool) (bool, error)
    65  }
    66  
    67  func (si *ServerInterceptor) Stream() grpc.StreamServerInterceptor {
    68  	return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
    69  		needSuperUser, err := requireSuperUser(info.FullMethod)
    70  		if err != nil {
    71  			return err
    72  		}
    73  
    74  		if err := si.authenticate(ss.Context(), needSuperUser); err != nil {
    75  			return err
    76  		}
    77  
    78  		return handler(srv, ss)
    79  	}
    80  }
    81  
    82  func (si *ServerInterceptor) Unary() grpc.UnaryServerInterceptor {
    83  	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    84  		needSuperUser, err := requireSuperUser(info.FullMethod)
    85  		if err != nil {
    86  			return nil, err
    87  		}
    88  
    89  		if err := si.authenticate(ctx, needSuperUser); err != nil {
    90  			return nil, err
    91  		}
    92  
    93  		return handler(ctx, req)
    94  	}
    95  }
    96  
    97  func (si *ServerInterceptor) Options() []grpc.ServerOption {
    98  	return []grpc.ServerOption{
    99  		grpc.ChainUnaryInterceptor(si.Unary()),
   100  		grpc.ChainStreamInterceptor(si.Stream()),
   101  	}
   102  }
   103  
   104  func requireSuperUser(path string) (bool, error) {
   105  	if SUPER_USER_RPC_METHODS[path] {
   106  		return true, nil
   107  	}
   108  
   109  	if CLONE_ADMIN_RPC_METHODS[path] {
   110  		return false, nil
   111  	}
   112  
   113  	return false, fmt.Errorf("unknown rpc method: %s", path)
   114  }
   115  
   116  // authenticate checks the incoming request for authentication credentials and validates them.  If the user is
   117  // legitimate, an authorization check is performed. If no error is returned, the user should be allowed to proceed.
   118  func (si *ServerInterceptor) authenticate(ctx context.Context, needsSuperUser bool) error {
   119  	ctx, err := si.AccessController.ApiAuthenticate(ctx)
   120  	if err != nil {
   121  		si.Lgr.Warnf("authentication failed: %s", err.Error())
   122  		return status.Error(codes.Unauthenticated, err.Error())
   123  	}
   124  
   125  	// Have a valid user in the context.  Check authorization.
   126  	if authorized, err := si.AccessController.ApiAuthorize(ctx, needsSuperUser); !authorized {
   127  		si.Lgr.Warnf("authorization failed: %s", err.Error())
   128  		return status.Error(codes.PermissionDenied, err.Error())
   129  	}
   130  
   131  	// Access Granted.
   132  	return nil
   133  }