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  }