github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/utils/grpc/interceptors/mfa.go (about)

     1  // Copyright 2023 Gravitational, 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 interceptors
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"log/slog"
    21  	"strings"
    22  
    23  	"github.com/gravitational/trace"
    24  	"github.com/gravitational/trace/trail"
    25  	"google.golang.org/grpc"
    26  
    27  	"github.com/gravitational/teleport/api/mfa"
    28  )
    29  
    30  // WithMFAUnaryInterceptor intercepts a GRPC client unary call to add MFA credentials
    31  // to the rpc call when an MFA response is provided through the context. Additionally,
    32  // when the call returns an error that indicates that MFA is required, this interceptor
    33  // will prompt for MFA using the given mfaCeremony and retry.
    34  func WithMFAUnaryInterceptor(mfaCeremony mfa.MFACeremony) grpc.UnaryClientInterceptor {
    35  	return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
    36  		// Check for MFA response passed through the context.
    37  		if mfaResp, err := mfa.MFAResponseFromContext(ctx); err == nil {
    38  			// If we find an MFA response passed through the context, attach it to the
    39  			// request. Note: this may still fail if the MFA response allows reuse and
    40  			// the specified endpoint doesn't allow reuse. In this case, the client
    41  			// prompts for MFA again below.
    42  			opts = append(opts, mfa.WithCredentials(mfaResp))
    43  		} else if !trace.IsNotFound(err) {
    44  			return trace.Wrap(err)
    45  		}
    46  
    47  		err := invoker(ctx, method, req, reply, cc, opts...)
    48  		if !errors.Is(trail.FromGRPC(err), &mfa.ErrAdminActionMFARequired) {
    49  			return err
    50  		}
    51  
    52  		// In this context, method looks like "/proto.<grpc-service-name>/<method-name>",
    53  		// we just want the method name.
    54  		splitMethod := strings.Split(method, "/")
    55  		readableMethodName := splitMethod[len(splitMethod)-1]
    56  		slog.DebugContext(ctx, "Retrying API request with Admin MFA", "method", readableMethodName)
    57  
    58  		// Start an MFA prompt that shares what API request caused MFA to be prompted.
    59  		// ex: MFA is required for admin-level API request: "CreateUser"
    60  		mfaResp, ceremonyErr := mfa.PerformAdminActionMFACeremony(ctx, mfaCeremony, false /*allowReuse*/)
    61  		if ceremonyErr != nil {
    62  			// If the client does not support MFA ceremonies, return the original error.
    63  			if errors.Is(ceremonyErr, &mfa.ErrMFANotSupported) {
    64  				return trail.FromGRPC(err)
    65  			} else if errors.Is(ceremonyErr, &mfa.ErrMFANotRequired) {
    66  				// This error should never occur since the auth server uses the same mechanism
    67  				// to check for an MFA requirement as it does to authorize said requirement.
    68  				return trace.Wrap(trail.FromGRPC(err), "server is reporting that MFA is not required when it is (this is a bug)")
    69  			}
    70  			return trace.NewAggregate(trail.FromGRPC(err), ceremonyErr)
    71  		}
    72  
    73  		return invoker(ctx, method, req, reply, cc, append(opts, mfa.WithCredentials(mfaResp))...)
    74  	}
    75  }