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 }