go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth_service/impl/model/readers_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 model 16 17 import ( 18 "context" 19 "fmt" 20 "strings" 21 "testing" 22 23 "github.com/golang/mock/gomock" 24 25 "go.chromium.org/luci/common/clock" 26 "go.chromium.org/luci/common/clock/testclock" 27 "go.chromium.org/luci/common/data/stringset" 28 "go.chromium.org/luci/gae/impl/memory" 29 "go.chromium.org/luci/gae/service/datastore" 30 "go.chromium.org/luci/server/auth" 31 "go.chromium.org/luci/server/auth/authtest" 32 33 "go.chromium.org/luci/auth_service/api/configspb" 34 "go.chromium.org/luci/auth_service/internal/configs/srvcfg/settingscfg" 35 "go.chromium.org/luci/auth_service/internal/gs" 36 37 . "github.com/smartystreets/goconvey/convey" 38 . "go.chromium.org/luci/common/testing/assertions" 39 ) 40 41 func testReader(ctx context.Context, email string) *AuthDBReader { 42 return &AuthDBReader{ 43 Kind: "AuthDBReader", 44 Parent: authDBReadersRootKey(ctx), 45 ID: email, 46 AuthorizedTS: testCreatedTS, 47 } 48 } 49 50 // getRawReaders is a helper function to get all AuthDBReaders in 51 // datastore. MUST be called from within a test case. 52 func getRawReaders(ctx context.Context) []*AuthDBReader { 53 q := datastore.NewQuery("AuthDBReader").Ancestor(authDBReadersRootKey(ctx)) 54 readers := []*AuthDBReader{} 55 So(datastore.GetAll(ctx, q, &readers), ShouldBeNil) 56 return readers 57 } 58 59 func TestIsAuthorizedReader(t *testing.T) { 60 t.Parallel() 61 62 Convey("IsAuthorizedReader works", t, func() { 63 ctx := memory.Use(context.Background()) 64 65 // Set up an authorized user. 66 So(datastore.Put(ctx, 67 testReader(ctx, "someone@example.com"), 68 ), ShouldBeNil) 69 70 Convey("true for authorized user", func() { 71 authorized, err := IsAuthorizedReader(ctx, "someone@example.com") 72 So(err, ShouldBeNil) 73 So(authorized, ShouldBeTrue) 74 }) 75 76 Convey("false for not authorized user", func() { 77 authorized, err := IsAuthorizedReader(ctx, "somebody@example.com") 78 So(err, ShouldBeNil) 79 So(authorized, ShouldBeFalse) 80 }) 81 }) 82 } 83 84 func TestGetAuthorizedEmails(t *testing.T) { 85 t.Parallel() 86 87 Convey("GetAuthorizedEmails returns all readers", t, func() { 88 ctx := memory.Use(context.Background()) 89 90 // No readers. 91 readers, err := GetAuthorizedEmails(ctx) 92 So(err, ShouldBeNil) 93 So(readers, ShouldBeEmpty) 94 95 // A couple of readers. 96 So(datastore.Put(ctx, 97 testReader(ctx, "adam@example.com"), 98 testReader(ctx, "eve@example.com"), 99 ), ShouldBeNil) 100 readers, err = GetAuthorizedEmails(ctx) 101 So(err, ShouldBeNil) 102 So(readers, ShouldEqual, 103 stringset.NewFromSlice("eve@example.com", "adam@example.com")) 104 }) 105 } 106 107 func TestAuthorizeReader(t *testing.T) { 108 t.Parallel() 109 110 Convey("AuthorizeReader works", t, func() { 111 ctx := memory.Use(context.Background()) 112 ctx = clock.Set(ctx, testclock.New(testModifiedTS)) 113 114 // Set up mock GS client 115 ctl := gomock.NewController(t) 116 mockClient := gs.NewMockedClient(ctx, ctl) 117 ctx = mockClient.Ctx 118 119 // Set up settings config. 120 cfg := &configspb.SettingsCfg{ 121 AuthDbGsPath: "chrome-infra-auth-test.appspot.com/auth-db", 122 } 123 So(settingscfg.SetConfig(ctx, cfg), ShouldBeNil) 124 125 Convey("disallows long emails", func() { 126 testEmail := strings.Repeat("a", MaxReaderEmailLength-12) + "@example.com" 127 So(AuthorizeReader(ctx, testEmail), ShouldErrLike, 128 "email is too long") 129 So(getRawReaders(ctx), ShouldBeEmpty) 130 }) 131 132 Convey("respects max reader count", func() { 133 // Set up lots of existing readers. 134 dummyReaders := make([]*AuthDBReader, MaxReaders) 135 for i := 0; i < MaxReaders; i++ { 136 dummyReaders[i] = testReader(ctx, 137 fmt.Sprintf("user-%d@example.com", i)) 138 } 139 So(datastore.Put(ctx, dummyReaders), ShouldBeNil) 140 So(getRawReaders(ctx), ShouldHaveLength, MaxReaders) 141 142 // Check authorizing an additional user fails. 143 So(AuthorizeReader(ctx, "someone@example.com"), ShouldErrLike, 144 "soft limit on GCS ACL entries") 145 So(getRawReaders(ctx), ShouldHaveLength, MaxReaders) 146 }) 147 148 Convey("email is recorded", func() { 149 // Define expected client calls. 150 gomock.InOrder( 151 mockClient.Client.EXPECT().UpdateReadACL(gomock.Any(), 152 gomock.Any(), stringset.NewFromSlice("someone@example.com")).Times(2), 153 mockClient.Client.EXPECT().Close().Times(1)) 154 155 So(AuthorizeReader(ctx, "someone@example.com"), ShouldBeNil) 156 So(getRawReaders(ctx), ShouldResembleProto, []*AuthDBReader{ 157 { 158 Kind: "AuthDBReader", 159 Parent: authDBReadersRootKey(ctx), 160 ID: "someone@example.com", 161 AuthorizedTS: testModifiedTS, 162 }, 163 }) 164 165 Convey("already authorized user is not duplicated", func() { 166 // Define expected client calls. 167 gomock.InOrder( 168 mockClient.Client.EXPECT().UpdateReadACL(gomock.Any(), 169 gomock.Any(), stringset.NewFromSlice("someone@example.com")).Times(2), 170 mockClient.Client.EXPECT().Close().Times(1)) 171 172 So(AuthorizeReader(ctx, "someone@example.com"), ShouldBeNil) 173 So(getRawReaders(ctx), ShouldHaveLength, 1) 174 }) 175 }) 176 }) 177 } 178 179 func TestDeauthorizeReader(t *testing.T) { 180 t.Parallel() 181 182 Convey("DeauthorizeReader works", t, func() { 183 ctx := memory.Use(context.Background()) 184 ctx = clock.Set(ctx, testclock.New(testModifiedTS)) 185 186 // Set up mock GS client 187 ctl := gomock.NewController(t) 188 mockClient := gs.NewMockedClient(ctx, ctl) 189 ctx = mockClient.Ctx 190 191 // Set up settings config. 192 cfg := &configspb.SettingsCfg{ 193 AuthDbGsPath: "chrome-infra-auth-test.appspot.com/auth-db", 194 } 195 So(settingscfg.SetConfig(ctx, cfg), ShouldBeNil) 196 197 // Add an authorized user. 198 So(datastore.Put(ctx, testReader(ctx, "someone@example.com")), 199 ShouldBeNil) 200 201 Convey("succeeds for non-authorized user", func() { 202 // Define expected client calls. 203 gomock.InOrder( 204 mockClient.Client.EXPECT().UpdateReadACL(gomock.Any(), 205 gomock.Any(), stringset.NewFromSlice("someone@example.com")).Times(2), 206 mockClient.Client.EXPECT().Close().Times(1)) 207 208 So(DeauthorizeReader(ctx, "unknown@example.com"), ShouldBeNil) 209 So(getRawReaders(ctx), ShouldResembleProto, []*AuthDBReader{ 210 testReader(ctx, "someone@example.com"), 211 }) 212 }) 213 214 Convey("removes the user", func() { 215 // Define expected client calls. 216 gomock.InOrder( 217 mockClient.Client.EXPECT().UpdateReadACL(gomock.Any(), 218 gomock.Any(), stringset.New(0)).Times(2), 219 mockClient.Client.EXPECT().Close().Times(1)) 220 221 So(DeauthorizeReader(ctx, "someone@example.com"), ShouldBeNil) 222 So(getRawReaders(ctx), ShouldBeEmpty) 223 }) 224 }) 225 } 226 227 func TestRevokeStaleReaderAccess(t *testing.T) { 228 t.Parallel() 229 230 Convey("RevokeStaleReaderAccess works", t, func() { 231 ctx := memory.Use(context.Background()) 232 ctx = clock.Set(ctx, testclock.New(testModifiedTS)) 233 234 // Set up mock GS client 235 ctl := gomock.NewController(t) 236 mockClient := gs.NewMockedClient(ctx, ctl) 237 ctx = mockClient.Ctx 238 239 // Set up settings config. 240 cfg := &configspb.SettingsCfg{ 241 AuthDbGsPath: "chrome-infra-auth-test.appspot.com/auth-db", 242 } 243 So(settingscfg.SetConfig(ctx, cfg), ShouldBeNil) 244 245 // Add existing readers. 246 err := datastore.Put(ctx, 247 testReader(ctx, "someone@example.com"), 248 testReader(ctx, "somebody@example.com"), 249 ) 250 So(err, ShouldBeNil) 251 252 // The group name for trusted accounts that are eligible to be 253 // readers. 254 trustedGroup := TrustedServicesGroup 255 256 Convey("revokes if not trusted", func() { 257 ctx = auth.WithState(ctx, &authtest.FakeState{ 258 Identity: "user:someone@example.com", 259 FakeDB: authtest.NewFakeDB( 260 authtest.MockMembership("user:someone@example.com", trustedGroup), 261 ), 262 }) 263 264 // Define expected client calls. 265 gomock.InOrder( 266 mockClient.Client.EXPECT().UpdateReadACL(gomock.Any(), 267 gomock.Any(), stringset.NewFromSlice("someone@example.com")).Times(2), 268 mockClient.Client.EXPECT().Close().Times(1)) 269 270 So(RevokeStaleReaderAccess(ctx, trustedGroup, false), ShouldBeNil) 271 So(getRawReaders(ctx), ShouldResembleProto, []*AuthDBReader{ 272 testReader(ctx, "someone@example.com"), 273 }) 274 }) 275 276 Convey("updates ACLs even if there are no deletions", func() { 277 ctx = auth.WithState(ctx, &authtest.FakeState{ 278 Identity: "user:someone@example.com", 279 FakeDB: authtest.NewFakeDB( 280 authtest.MockMembership("user:someone@example.com", trustedGroup), 281 authtest.MockMembership("user:somebody@example.com", trustedGroup), 282 ), 283 }) 284 285 // Define expected client calls. 286 gomock.InOrder( 287 mockClient.Client.EXPECT().UpdateReadACL( 288 gomock.Any(), 289 gomock.Any(), 290 stringset.NewFromSlice("someone@example.com", "somebody@example.com"), 291 ).Times(2), 292 mockClient.Client.EXPECT().Close().Times(1)) 293 294 So(RevokeStaleReaderAccess(ctx, trustedGroup, false), ShouldBeNil) 295 So(getRawReaders(ctx), ShouldResembleProto, []*AuthDBReader{ 296 testReader(ctx, "somebody@example.com"), 297 testReader(ctx, "someone@example.com"), 298 }) 299 }) 300 }) 301 }