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 }