go.temporal.io/server@v1.23.0/common/authorization/interceptor_test.go (about)

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package authorization
    26  
    27  import (
    28  	"context"
    29  	"testing"
    30  
    31  	"github.com/golang/mock/gomock"
    32  	"github.com/stretchr/testify/require"
    33  	"github.com/stretchr/testify/suite"
    34  	"go.temporal.io/api/workflowservice/v1"
    35  	"go.temporal.io/api/workflowservicemock/v1"
    36  	"google.golang.org/grpc"
    37  	"google.golang.org/grpc/metadata"
    38  
    39  	"go.temporal.io/server/common/log"
    40  	"go.temporal.io/server/common/metrics"
    41  )
    42  
    43  const (
    44  	testNamespace string = "test-namespace"
    45  )
    46  
    47  var (
    48  	ctx                           = context.Background()
    49  	describeNamespaceRequest      = &workflowservice.DescribeNamespaceRequest{Namespace: testNamespace}
    50  	describeNamespaceTarget       = &CallTarget{Namespace: testNamespace, Request: describeNamespaceRequest, APIName: "/temporal.api.workflowservice.v1.WorkflowService/DescribeNamespace"}
    51  	describeNamespaceInfo         = &grpc.UnaryServerInfo{FullMethod: "/temporal.api.workflowservice.v1.WorkflowService/DescribeNamespace"}
    52  	startWorkflowExecutionRequest = &workflowservice.StartWorkflowExecutionRequest{Namespace: testNamespace}
    53  	startWorkflowExecutionTarget  = &CallTarget{Namespace: testNamespace, Request: startWorkflowExecutionRequest, APIName: "/temporal.api.workflowservice.v1.WorkflowService/StartWorkflowExecution"}
    54  	startWorkflowExecutionInfo    = &grpc.UnaryServerInfo{FullMethod: "/temporal.api.workflowservice.v1.WorkflowService/StartWorkflowExecution"}
    55  )
    56  
    57  type (
    58  	authorizerInterceptorSuite struct {
    59  		suite.Suite
    60  		*require.Assertions
    61  
    62  		controller          *gomock.Controller
    63  		mockFrontendHandler *workflowservicemock.MockWorkflowServiceServer
    64  		mockAuthorizer      *MockAuthorizer
    65  		mockMetricsHandler  *metrics.MockHandler
    66  		interceptor         grpc.UnaryServerInterceptor
    67  		handler             grpc.UnaryHandler
    68  		mockClaimMapper     *MockClaimMapper
    69  	}
    70  )
    71  
    72  func TestAuthorizerInterceptorSuite(t *testing.T) {
    73  	s := new(authorizerInterceptorSuite)
    74  	suite.Run(t, s)
    75  }
    76  
    77  func (s *authorizerInterceptorSuite) SetupTest() {
    78  	s.Assertions = require.New(s.T())
    79  	s.controller = gomock.NewController(s.T())
    80  
    81  	s.mockFrontendHandler = workflowservicemock.NewMockWorkflowServiceServer(s.controller)
    82  	s.mockAuthorizer = NewMockAuthorizer(s.controller)
    83  	s.mockMetricsHandler = metrics.NewMockHandler(s.controller)
    84  	s.mockMetricsHandler.EXPECT().WithTags(metrics.OperationTag(metrics.AuthorizationScope)).Return(s.mockMetricsHandler).AnyTimes()
    85  	s.mockMetricsHandler.EXPECT().Timer(metrics.ServiceAuthorizationLatency.Name()).Return(metrics.NoopTimerMetricFunc).AnyTimes()
    86  	s.mockClaimMapper = NewMockClaimMapper(s.controller)
    87  	s.interceptor = NewAuthorizationInterceptor(
    88  		s.mockClaimMapper,
    89  		s.mockAuthorizer,
    90  		s.mockMetricsHandler,
    91  		log.NewNoopLogger(),
    92  		nil,
    93  		"",
    94  		"",
    95  	)
    96  	s.handler = func(ctx context.Context, req interface{}) (interface{}, error) { return true, nil }
    97  }
    98  
    99  func (s *authorizerInterceptorSuite) TearDownTest() {
   100  	s.controller.Finish()
   101  }
   102  
   103  func (s *authorizerInterceptorSuite) TestIsAuthorized() {
   104  	s.mockAuthorizer.EXPECT().Authorize(ctx, nil, describeNamespaceTarget).
   105  		Return(Result{Decision: DecisionAllow}, nil)
   106  
   107  	res, err := s.interceptor(ctx, describeNamespaceRequest, describeNamespaceInfo, s.handler)
   108  	s.True(res.(bool))
   109  	s.NoError(err)
   110  }
   111  
   112  func (s *authorizerInterceptorSuite) TestIsAuthorizedWithNamespace() {
   113  	s.mockAuthorizer.EXPECT().Authorize(ctx, nil, startWorkflowExecutionTarget).
   114  		Return(Result{Decision: DecisionAllow}, nil)
   115  
   116  	res, err := s.interceptor(ctx, startWorkflowExecutionRequest, startWorkflowExecutionInfo, s.handler)
   117  	s.True(res.(bool))
   118  	s.NoError(err)
   119  }
   120  
   121  func (s *authorizerInterceptorSuite) TestIsUnauthorized() {
   122  	s.mockAuthorizer.EXPECT().Authorize(ctx, nil, describeNamespaceTarget).
   123  		Return(Result{Decision: DecisionDeny}, nil)
   124  	s.mockMetricsHandler.EXPECT().Counter(metrics.ServiceErrUnauthorizedCounter.Name()).Return(metrics.NoopCounterMetricFunc)
   125  
   126  	res, err := s.interceptor(ctx, describeNamespaceRequest, describeNamespaceInfo, s.handler)
   127  	s.Nil(res)
   128  	s.Error(err)
   129  }
   130  
   131  func (s *authorizerInterceptorSuite) TestAuthorizationFailed() {
   132  	s.mockAuthorizer.EXPECT().Authorize(ctx, nil, describeNamespaceTarget).
   133  		Return(Result{Decision: DecisionDeny}, errUnauthorized)
   134  	s.mockMetricsHandler.EXPECT().Counter(metrics.ServiceErrAuthorizeFailedCounter.Name()).Return(metrics.NoopCounterMetricFunc)
   135  
   136  	res, err := s.interceptor(ctx, describeNamespaceRequest, describeNamespaceInfo, s.handler)
   137  	s.Nil(res)
   138  	s.Error(err)
   139  }
   140  
   141  func (s *authorizerInterceptorSuite) TestNoopClaimMapperWithoutTLS() {
   142  	admin := &Claims{System: RoleAdmin}
   143  	s.mockAuthorizer.EXPECT().Authorize(gomock.Any(), admin, describeNamespaceTarget).
   144  		DoAndReturn(func(ctx context.Context, caller *Claims, target *CallTarget) (Result, error) {
   145  			// check that claims are present in ctx also
   146  			s.Equal(admin, ctx.Value(MappedClaims))
   147  
   148  			return Result{Decision: DecisionAllow}, nil
   149  		})
   150  
   151  	interceptor := NewAuthorizationInterceptor(
   152  		NewNoopClaimMapper(),
   153  		s.mockAuthorizer,
   154  		s.mockMetricsHandler,
   155  		log.NewNoopLogger(),
   156  		nil,
   157  		"",
   158  		"",
   159  	)
   160  	_, err := interceptor(ctx, describeNamespaceRequest, describeNamespaceInfo, s.handler)
   161  	s.NoError(err)
   162  }
   163  
   164  func (s *authorizerInterceptorSuite) TestAlternateHeaders() {
   165  	interceptor := NewAuthorizationInterceptor(
   166  		s.mockClaimMapper,
   167  		NewNoopAuthorizer(),
   168  		s.mockMetricsHandler,
   169  		log.NewNoopLogger(),
   170  		nil,
   171  		"custom-header",
   172  		"custom-extra-header",
   173  	)
   174  
   175  	cases := []struct {
   176  		md       metadata.MD
   177  		authInfo *AuthInfo
   178  	}{
   179  		{
   180  			metadata.Pairs(
   181  				"custom-header", "the-token",
   182  				"custom-extra-header", "more stuff",
   183  			),
   184  			&AuthInfo{
   185  				AuthToken: "the-token",
   186  				ExtraData: "more stuff",
   187  			},
   188  		},
   189  		{
   190  			metadata.Pairs(
   191  				"custom-header", "the-token",
   192  				"authorization-extras", "this gets ignored",
   193  			),
   194  			&AuthInfo{
   195  				AuthToken: "the-token",
   196  			},
   197  		},
   198  		{
   199  			metadata.Pairs(
   200  				"custom-extra-header", "missing main header, this gets ignored",
   201  			),
   202  			nil,
   203  		},
   204  	}
   205  	for _, testCase := range cases {
   206  		if testCase.authInfo != nil {
   207  			s.mockClaimMapper.EXPECT().GetClaims(testCase.authInfo).Return(&Claims{System: RoleAdmin}, nil)
   208  		}
   209  		inCtx := metadata.NewIncomingContext(ctx, testCase.md)
   210  		_, err := interceptor(inCtx, describeNamespaceRequest, describeNamespaceInfo, s.handler)
   211  		s.NoError(err)
   212  	}
   213  }