go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/swarming/server/rpcs/swarming_get_permissions_test.go (about)

     1  // Copyright 2023 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 rpcs
    16  
    17  import (
    18  	"context"
    19  	"testing"
    20  
    21  	"google.golang.org/grpc/codes"
    22  
    23  	"go.chromium.org/luci/auth/identity"
    24  	"go.chromium.org/luci/gae/impl/memory"
    25  	"go.chromium.org/luci/gae/service/datastore"
    26  	"go.chromium.org/luci/server/auth/authtest"
    27  	"go.chromium.org/luci/server/auth/realms"
    28  
    29  	apipb "go.chromium.org/luci/swarming/proto/api_v2"
    30  	configpb "go.chromium.org/luci/swarming/proto/config"
    31  	"go.chromium.org/luci/swarming/server/acls"
    32  	"go.chromium.org/luci/swarming/server/model"
    33  
    34  	. "github.com/smartystreets/goconvey/convey"
    35  	. "go.chromium.org/luci/common/testing/assertions"
    36  )
    37  
    38  func TestSwarmingServer(t *testing.T) {
    39  	t.Parallel()
    40  
    41  	Convey("With mocks", t, func() {
    42  		const (
    43  			adminID      identity.Identity = "user:admin@example.com"
    44  			unknownID    identity.Identity = "user:unknown@example.com"
    45  			authorizedID identity.Identity = "user:authorized@example.com"
    46  			submitterID  identity.Identity = "user:submitter@example.com"
    47  
    48  			allowedTaskID   = "65aba3a3e6b99310"
    49  			forbiddenTaskID = "65aba3a3e6b99410"
    50  			unknownTaskID   = "65aba3a3e6b99510"
    51  		)
    52  
    53  		configs := MockedConfigs{
    54  			Settings: &configpb.SettingsCfg{
    55  				Auth: &configpb.AuthSettings{
    56  					AdminsGroup: "admins",
    57  				},
    58  			},
    59  			Pools: &configpb.PoolsCfg{
    60  				Pool: []*configpb.Pool{
    61  					{
    62  						Name:  []string{"visible-pool-1", "visible-pool-2"},
    63  						Realm: "project:visible-realm",
    64  					},
    65  					{
    66  						Name:  []string{"hidden-pool-1", "hidden-pool-2"},
    67  						Realm: "project:hidden-realm",
    68  					},
    69  				},
    70  			},
    71  			Bots: &configpb.BotsCfg{
    72  				TrustedDimensions: []string{"pool"},
    73  				BotGroup: []*configpb.BotGroup{
    74  					{
    75  						BotId:      []string{"visible-bot"},
    76  						Dimensions: []string{"pool:visible-pool-1"},
    77  						Auth: []*configpb.BotAuth{
    78  							{
    79  								RequireLuciMachineToken: true,
    80  							},
    81  						},
    82  					},
    83  				},
    84  			},
    85  		}
    86  		db := authtest.NewFakeDB(
    87  			authtest.MockMembership(adminID, "admins"),
    88  		)
    89  		authorized := []realms.Permission{
    90  			acls.PermPoolsDeleteBot,
    91  			acls.PermPoolsTerminateBot,
    92  			acls.PermPoolsCreateBot,
    93  			acls.PermPoolsCancelTask,
    94  			acls.PermPoolsListBots,
    95  			acls.PermPoolsListTasks,
    96  			acls.PermTasksCancel,
    97  		}
    98  		for _, perm := range authorized {
    99  			db.AddMocks(authtest.MockPermission(authorizedID, "project:visible-realm", perm))
   100  		}
   101  
   102  		expectedVisiblePools := []string{
   103  			"visible-pool-1",
   104  			"visible-pool-2",
   105  		}
   106  
   107  		ctx := memory.Use(context.Background())
   108  
   109  		createFakeTask(ctx, allowedTaskID, "project:task-realm", "visible-pool-1", submitterID)
   110  		createFakeTask(ctx, forbiddenTaskID, "project:hidden-realm", "hidden-pool-1", unknownID)
   111  
   112  		srv := SwarmingServer{}
   113  
   114  		callWithErr := func(caller identity.Identity, botID, taskID string, tags []string) (*apipb.ClientPermissions, error) {
   115  			ctx := MockRequestState(ctx, &MockedRequestState{
   116  				Caller:  caller,
   117  				AuthDB:  db,
   118  				Configs: configs,
   119  			})
   120  			return srv.GetPermissions(ctx, &apipb.PermissionsRequest{
   121  				BotId:  botID,
   122  				TaskId: taskID,
   123  				Tags:   tags,
   124  			})
   125  		}
   126  
   127  		call := func(caller identity.Identity, botID, taskID string, tags []string) *apipb.ClientPermissions {
   128  			resp, err := callWithErr(caller, botID, taskID, tags)
   129  			So(err, ShouldBeNil)
   130  			return resp
   131  		}
   132  
   133  		Convey("Admin", func() {
   134  			So(call(adminID, "", allowedTaskID, nil), ShouldResembleProto, &apipb.ClientPermissions{
   135  				DeleteBot:         true,
   136  				DeleteBots:        true,
   137  				TerminateBot:      true,
   138  				GetConfigs:        false,
   139  				PutConfigs:        false,
   140  				CancelTask:        true,
   141  				GetBootstrapToken: true,
   142  				CancelTasks:       true,
   143  				ListBots: []string{
   144  					"hidden-pool-1",
   145  					"hidden-pool-2",
   146  					"visible-pool-1",
   147  					"visible-pool-2",
   148  				},
   149  				ListTasks: []string{
   150  					"hidden-pool-1",
   151  					"hidden-pool-2",
   152  					"visible-pool-1",
   153  					"visible-pool-2",
   154  				},
   155  			})
   156  		})
   157  
   158  		Convey("Unknown", func() {
   159  			So(call(unknownID, "", allowedTaskID, nil), ShouldResembleProto, &apipb.ClientPermissions{
   160  				// All empty.
   161  			})
   162  		})
   163  
   164  		Convey("Authorized pools", func() {
   165  			So(call(authorizedID, "", "", []string{"pool:visible-pool-1"}),
   166  				ShouldResembleProto,
   167  				&apipb.ClientPermissions{
   168  					CancelTask:  true,
   169  					CancelTasks: true,
   170  					DeleteBots:  true,
   171  					ListBots:    expectedVisiblePools,
   172  					ListTasks:   expectedVisiblePools,
   173  				},
   174  			)
   175  		})
   176  
   177  		Convey("Hidden pools", func() {
   178  			So(call(authorizedID, "", "", []string{"pool:hidden-pool-1"}),
   179  				ShouldResembleProto,
   180  				&apipb.ClientPermissions{
   181  					ListBots:  expectedVisiblePools,
   182  					ListTasks: expectedVisiblePools,
   183  				},
   184  			)
   185  		})
   186  
   187  		Convey("Accessing task", func() {
   188  			So(call(authorizedID, "", allowedTaskID, nil),
   189  				ShouldResembleProto,
   190  				&apipb.ClientPermissions{
   191  					CancelTask: true,
   192  					ListBots:   expectedVisiblePools,
   193  					ListTasks:  expectedVisiblePools,
   194  				},
   195  			)
   196  
   197  			So(call(authorizedID, "", forbiddenTaskID, nil),
   198  				ShouldResembleProto,
   199  				&apipb.ClientPermissions{
   200  					ListBots:  expectedVisiblePools,
   201  					ListTasks: expectedVisiblePools,
   202  				},
   203  			)
   204  
   205  			// We allow leaking existence of a task ID for better error message. Task
   206  			// ID is mostly random and it doesn't have any private bits in it.
   207  			_, err := callWithErr(authorizedID, "", unknownTaskID, nil)
   208  			So(err, ShouldHaveGRPCStatus, codes.NotFound)
   209  		})
   210  
   211  		Convey("Accessing bot", func() {
   212  			So(call(authorizedID, "visible-bot", "", nil),
   213  				ShouldResembleProto,
   214  				&apipb.ClientPermissions{
   215  					DeleteBot:    true,
   216  					TerminateBot: true,
   217  					ListBots:     expectedVisiblePools,
   218  					ListTasks:    expectedVisiblePools,
   219  				},
   220  			)
   221  
   222  			So(call(authorizedID, "hidden-bot", "", nil),
   223  				ShouldResembleProto,
   224  				&apipb.ClientPermissions{
   225  					ListBots:  expectedVisiblePools,
   226  					ListTasks: expectedVisiblePools,
   227  				},
   228  			)
   229  		})
   230  	})
   231  }
   232  
   233  func createFakeTask(ctx context.Context, taskID, realm, pool string, submitterID identity.Identity) {
   234  	key, err := model.TaskIDToRequestKey(ctx, taskID)
   235  	So(err, ShouldBeNil)
   236  	So(datastore.Put(ctx, &model.TaskRequest{
   237  		Key:           key,
   238  		Realm:         realm,
   239  		Authenticated: submitterID,
   240  		TaskSlices: []model.TaskSlice{
   241  			{
   242  				Properties: model.TaskProperties{
   243  					Dimensions: map[string][]string{
   244  						"pool": {pool},
   245  					},
   246  				},
   247  			},
   248  		},
   249  	}), ShouldBeNil)
   250  }