go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth_service/services/frontend/subscription/handler_test.go (about)

     1  // Copyright 2024 The LUCI Authors.
     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 subscription
    16  
    17  import (
    18  	"context"
    19  	"net/http"
    20  	"net/http/httptest"
    21  	"testing"
    22  	"time"
    23  
    24  	"github.com/golang/mock/gomock"
    25  	"google.golang.org/grpc/codes"
    26  	"google.golang.org/grpc/status"
    27  
    28  	"go.chromium.org/luci/common/clock"
    29  	"go.chromium.org/luci/common/clock/testclock"
    30  	"go.chromium.org/luci/common/data/stringset"
    31  	"go.chromium.org/luci/gae/impl/memory"
    32  	"go.chromium.org/luci/server/auth"
    33  	"go.chromium.org/luci/server/auth/authtest"
    34  	"go.chromium.org/luci/server/router"
    35  
    36  	"go.chromium.org/luci/auth_service/api/configspb"
    37  	"go.chromium.org/luci/auth_service/impl/model"
    38  	"go.chromium.org/luci/auth_service/internal/configs/srvcfg/settingscfg"
    39  	"go.chromium.org/luci/auth_service/internal/gs"
    40  	"go.chromium.org/luci/auth_service/internal/pubsub"
    41  
    42  	. "github.com/smartystreets/goconvey/convey"
    43  	. "go.chromium.org/luci/common/testing/assertions"
    44  )
    45  
    46  var (
    47  	testModifiedTS = time.Date(2021, time.August, 16, 12, 20, 0, 0, time.UTC)
    48  )
    49  
    50  func TestCheckAccess(t *testing.T) {
    51  	t.Parallel()
    52  
    53  	Convey("CheckAccess works", t, func() {
    54  		ctx := memory.Use(context.Background())
    55  		ctx = clock.Set(ctx, testclock.New(testModifiedTS))
    56  
    57  		// Set up mock Pubsub client
    58  		ctl := gomock.NewController(t)
    59  		mockPubsubClient := pubsub.NewMockedClient(ctx, ctl)
    60  		ctx = mockPubsubClient.Ctx
    61  		policy := pubsub.StubPolicy("someone@example.com")
    62  
    63  		// Set up settings config.
    64  		cfg := &configspb.SettingsCfg{}
    65  		So(settingscfg.SetConfig(ctx, cfg), ShouldBeNil)
    66  
    67  		// Set up an authorized user.
    68  		So(model.AuthorizeReader(ctx, "someone@example.com"), ShouldBeNil)
    69  
    70  		Convey("user must use email-based auth", func() {
    71  			ctx = auth.WithState(ctx, &authtest.FakeState{
    72  				Identity: "anonymous:anonymous",
    73  			})
    74  			rw := httptest.NewRecorder()
    75  			rctx := &router.Context{
    76  				Request: (&http.Request{}).WithContext(ctx),
    77  				Writer:  rw,
    78  			}
    79  			err := CheckAccess(rctx)
    80  			So(err, ShouldErrLike, "error getting caller email")
    81  			So(status.Code(err), ShouldEqual, codes.InvalidArgument)
    82  			So(rw.Body.Bytes(), ShouldBeEmpty)
    83  		})
    84  
    85  		Convey("false for unauthorized", func() {
    86  			// Set expected Pubsub client calls.
    87  			gomock.InOrder(
    88  				mockPubsubClient.Client.EXPECT().GetIAMPolicy(gomock.Any()).Return(policy, nil).Times(1),
    89  				mockPubsubClient.Client.EXPECT().Close().Times(1))
    90  
    91  			ctx = auth.WithState(ctx, &authtest.FakeState{
    92  				Identity: "user:somebody@example.com",
    93  			})
    94  			rw := httptest.NewRecorder()
    95  			rctx := &router.Context{
    96  				Request: (&http.Request{}).WithContext(ctx),
    97  				Writer:  rw,
    98  			}
    99  			err := CheckAccess(rctx)
   100  			So(err, ShouldBeNil)
   101  			expectedBlob := []byte(`{"topic":"projects/app/topics/auth-db-changed","authorized":false,"gs":{"auth_db_gs_path":"","authorized":false}}`)
   102  			So(rw.Body.Bytes(), ShouldEqual, expectedBlob)
   103  		})
   104  
   105  		Convey("true for authorized", func() {
   106  			// Set expected Pubsub client calls.
   107  			gomock.InOrder(
   108  				mockPubsubClient.Client.EXPECT().GetIAMPolicy(gomock.Any()).Return(policy, nil).Times(1),
   109  				mockPubsubClient.Client.EXPECT().Close().Times(1))
   110  
   111  			ctx = auth.WithState(ctx, &authtest.FakeState{
   112  				Identity: "user:someone@example.com",
   113  			})
   114  			rw := httptest.NewRecorder()
   115  			rctx := &router.Context{
   116  				Request: (&http.Request{}).WithContext(ctx),
   117  				Writer:  rw,
   118  			}
   119  			err := CheckAccess(rctx)
   120  			So(err, ShouldBeNil)
   121  			expectedBlob := []byte(`{"topic":"projects/app/topics/auth-db-changed","authorized":true,"gs":{"auth_db_gs_path":"","authorized":true}}`)
   122  			So(rw.Body.Bytes(), ShouldEqual, expectedBlob)
   123  		})
   124  	})
   125  }
   126  
   127  func TestAuthorize(t *testing.T) {
   128  	t.Parallel()
   129  
   130  	Convey("Authorize works", t, func() {
   131  		ctx := memory.Use(context.Background())
   132  		ctx = clock.Set(ctx, testclock.New(testModifiedTS))
   133  
   134  		// Set up mock Pubsub and GS client
   135  		ctl := gomock.NewController(t)
   136  		mockGSClient := gs.NewMockedClient(ctx, ctl)
   137  		mockPubsubClient := pubsub.NewMockedClient(mockGSClient.Ctx, ctl)
   138  		ctx = mockPubsubClient.Ctx
   139  
   140  		// Set up settings config.
   141  		cfg := &configspb.SettingsCfg{
   142  			AuthDbGsPath: "chrome-infra-auth-test.appspot.com/auth-db",
   143  		}
   144  		So(settingscfg.SetConfig(ctx, cfg), ShouldBeNil)
   145  
   146  		Convey("user must use email-based auth", func() {
   147  			ctx = auth.WithState(ctx, &authtest.FakeState{
   148  				Identity: "anonymous:anonymous",
   149  			})
   150  			rw := httptest.NewRecorder()
   151  			rctx := &router.Context{
   152  				Request: (&http.Request{}).WithContext(ctx),
   153  				Writer:  rw,
   154  			}
   155  			err := Authorize(rctx)
   156  			So(err, ShouldErrLike, "error getting caller email")
   157  			So(status.Code(err), ShouldEqual, codes.InvalidArgument)
   158  			So(rw.Body.Bytes(), ShouldBeEmpty)
   159  		})
   160  
   161  		Convey("authorizes a new user", func() {
   162  			// Set expected GS client calls from updating ACLs.
   163  			gomock.InOrder(
   164  				mockGSClient.Client.EXPECT().UpdateReadACL(
   165  					gomock.Any(), gomock.Any(), stringset.NewFromSlice("someone@example.com")).Times(2),
   166  				mockGSClient.Client.EXPECT().Close().Times(1))
   167  
   168  			// Set expected Pubsub client calls.
   169  			gomock.InOrder(
   170  				mockPubsubClient.Client.EXPECT().GetIAMPolicy(gomock.Any()).Return(pubsub.StubPolicy(), nil).Times(1),
   171  				mockPubsubClient.Client.EXPECT().SetIAMPolicy(gomock.Any(), gomock.Any()).Times(1),
   172  				mockPubsubClient.Client.EXPECT().Close().Times(1))
   173  
   174  			ctx = auth.WithState(ctx, &authtest.FakeState{
   175  				Identity: "user:someone@example.com",
   176  			})
   177  			rw := httptest.NewRecorder()
   178  			rctx := &router.Context{
   179  				Request: (&http.Request{}).WithContext(ctx),
   180  				Writer:  rw,
   181  			}
   182  			err := Authorize(rctx)
   183  			So(err, ShouldBeNil)
   184  			expectedBlob := []byte(`{"topic":"projects/app/topics/auth-db-changed","authorized":true,"gs":{"auth_db_gs_path":"chrome-infra-auth-test.appspot.com/auth-db","authorized":true}}`)
   185  			So(rw.Body.Bytes(), ShouldEqual, expectedBlob)
   186  		})
   187  
   188  		Convey("succeeds for authorized user", func() {
   189  			// Set expected GS client calls for test setup, followed by
   190  			// expected authorization of the user from Subscribe.
   191  			gomock.InOrder(
   192  				mockGSClient.Client.EXPECT().UpdateReadACL(
   193  					gomock.Any(), gomock.Any(), stringset.NewFromSlice("somebody@example.com")).Times(2),
   194  				mockGSClient.Client.EXPECT().Close().Times(1),
   195  				mockGSClient.Client.EXPECT().UpdateReadACL(
   196  					gomock.Any(), gomock.Any(), stringset.NewFromSlice("somebody@example.com")).Times(2),
   197  				mockGSClient.Client.EXPECT().Close().Times(1))
   198  
   199  			// Set expected Pubsub client calls.
   200  			gomock.InOrder(
   201  				mockPubsubClient.Client.EXPECT().GetIAMPolicy(gomock.Any()).Return(pubsub.StubPolicy("somebody@example.com"), nil).Times(1),
   202  				mockPubsubClient.Client.EXPECT().Close().Times(1))
   203  
   204  			// Set up an authorized user.
   205  			So(model.AuthorizeReader(ctx, "somebody@example.com"), ShouldBeNil)
   206  
   207  			ctx = auth.WithState(ctx, &authtest.FakeState{
   208  				Identity: "user:somebody@example.com",
   209  			})
   210  			rw := httptest.NewRecorder()
   211  			rctx := &router.Context{
   212  				Request: (&http.Request{}).WithContext(ctx),
   213  				Writer:  rw,
   214  			}
   215  			err := Authorize(rctx)
   216  			So(err, ShouldBeNil)
   217  			expectedBlob := []byte(`{"topic":"projects/app/topics/auth-db-changed","authorized":true,"gs":{"auth_db_gs_path":"chrome-infra-auth-test.appspot.com/auth-db","authorized":true}}`)
   218  			So(rw.Body.Bytes(), ShouldEqual, expectedBlob)
   219  		})
   220  	})
   221  }
   222  
   223  func TestDeauthorize(t *testing.T) {
   224  	t.Parallel()
   225  
   226  	Convey("Deauthorize works", t, func() {
   227  		ctx := memory.Use(context.Background())
   228  		ctx = clock.Set(ctx, testclock.New(testModifiedTS))
   229  
   230  		// Set up mock GS client and mock Pubsub client
   231  		ctl := gomock.NewController(t)
   232  		mockGSClient := gs.NewMockedClient(ctx, ctl)
   233  		mockPubsubClient := pubsub.NewMockedClient(mockGSClient.Ctx, ctl)
   234  		ctx = mockPubsubClient.Ctx
   235  
   236  		// Set up settings config.
   237  		cfg := &configspb.SettingsCfg{
   238  			AuthDbGsPath: "chrome-infra-auth-test.appspot.com/auth-db",
   239  		}
   240  		So(settingscfg.SetConfig(ctx, cfg), ShouldBeNil)
   241  
   242  		Convey("user must use email-based auth", func() {
   243  			ctx = auth.WithState(ctx, &authtest.FakeState{
   244  				Identity: "anonymous:anonymous",
   245  			})
   246  			rw := httptest.NewRecorder()
   247  			rctx := &router.Context{
   248  				Request: (&http.Request{}).WithContext(ctx),
   249  				Writer:  rw,
   250  			}
   251  			err := Deauthorize(rctx)
   252  			So(err, ShouldErrLike, "error getting caller email")
   253  			So(status.Code(err), ShouldEqual, codes.InvalidArgument)
   254  			So(rw.Body.Bytes(), ShouldBeEmpty)
   255  		})
   256  
   257  		Convey("revokes for authorized user", func() {
   258  			// Set expected GS client calls for test setup, followed by
   259  			// expected deauthorization of the user from Unsubscribe.
   260  			gomock.InOrder(
   261  				mockGSClient.Client.EXPECT().UpdateReadACL(
   262  					gomock.Any(), gomock.Any(), stringset.NewFromSlice("someone@example.com")).Times(2),
   263  				mockGSClient.Client.EXPECT().Close().Times(1),
   264  				mockGSClient.Client.EXPECT().UpdateReadACL(
   265  					gomock.Any(), gomock.Any(), stringset.Set{}).Times(2),
   266  				mockGSClient.Client.EXPECT().Close().Times(1))
   267  
   268  			// Set expected Pubsub client calls.
   269  			gomock.InOrder(
   270  				mockPubsubClient.Client.EXPECT().GetIAMPolicy(gomock.Any()).Return(pubsub.StubPolicy("someone@example.com"), nil).Times(1),
   271  				mockPubsubClient.Client.EXPECT().SetIAMPolicy(gomock.Any(), gomock.Any()).Times(1),
   272  				mockPubsubClient.Client.EXPECT().Close().Times(1))
   273  
   274  			// Set up an authorized user.
   275  			So(model.AuthorizeReader(ctx, "someone@example.com"), ShouldBeNil)
   276  
   277  			ctx = auth.WithState(ctx, &authtest.FakeState{
   278  				Identity: "user:someone@example.com",
   279  			})
   280  			rw := httptest.NewRecorder()
   281  			rctx := &router.Context{
   282  				Request: (&http.Request{}).WithContext(ctx),
   283  				Writer:  rw,
   284  			}
   285  			err := Deauthorize(rctx)
   286  			So(err, ShouldBeNil)
   287  			expectedBlob := []byte(`{"topic":"projects/app/topics/auth-db-changed","authorized":false,"gs":{"auth_db_gs_path":"chrome-infra-auth-test.appspot.com/auth-db","authorized":false}}`)
   288  			So(rw.Body.Bytes(), ShouldEqual, expectedBlob)
   289  		})
   290  
   291  		Convey("succeeds for unauthorized user", func() {
   292  			// Set expected client calls from updating ACLs.
   293  			gomock.InOrder(
   294  				mockGSClient.Client.EXPECT().UpdateReadACL(
   295  					gomock.Any(), gomock.Any(), stringset.Set{}).Times(2),
   296  				mockGSClient.Client.EXPECT().Close().Times(1))
   297  
   298  			// Set expected Pubsub client calls.
   299  			gomock.InOrder(
   300  				mockPubsubClient.Client.EXPECT().GetIAMPolicy(gomock.Any()).Return(pubsub.StubPolicy(), nil).Times(1),
   301  				mockPubsubClient.Client.EXPECT().Close().Times(1))
   302  
   303  			ctx = auth.WithState(ctx, &authtest.FakeState{
   304  				Identity: "user:somebody@example.com",
   305  			})
   306  			rw := httptest.NewRecorder()
   307  			rctx := &router.Context{
   308  				Request: (&http.Request{}).WithContext(ctx),
   309  				Writer:  rw,
   310  			}
   311  			err := Deauthorize(rctx)
   312  			So(err, ShouldBeNil)
   313  			expectedBlob := []byte(`{"topic":"projects/app/topics/auth-db-changed","authorized":false,"gs":{"auth_db_gs_path":"chrome-infra-auth-test.appspot.com/auth-db","authorized":false}}`)
   314  			So(rw.Body.Bytes(), ShouldEqual, expectedBlob)
   315  		})
   316  	})
   317  }