go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth_service/internal/realmsinternals/expansion_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 package realmsinternals 15 16 import ( 17 "fmt" 18 "testing" 19 20 realmsconf "go.chromium.org/luci/common/proto/realms" 21 "go.chromium.org/luci/config" 22 "go.chromium.org/luci/server/auth/service/protocol" 23 24 "go.chromium.org/luci/auth_service/api/configspb" 25 "go.chromium.org/luci/auth_service/internal/permissions" 26 27 . "github.com/smartystreets/goconvey/convey" 28 . "go.chromium.org/luci/common/testing/assertions" 29 ) 30 31 func testPermissionsDB(implicitRootBindings bool) *permissions.PermissionsDB { 32 db := permissions.NewPermissionsDB(&configspb.PermissionsConfig{ 33 Role: []*configspb.PermissionsConfig_Role{ 34 { 35 Name: "role/dev.a", 36 Permissions: []*protocol.Permission{ 37 { 38 Name: "luci.dev.p1", 39 }, 40 { 41 Name: "luci.dev.p2", 42 }, 43 }, 44 }, 45 { 46 Name: "role/dev.b", 47 Permissions: []*protocol.Permission{ 48 { 49 Name: "luci.dev.p2", 50 }, 51 { 52 Name: "luci.dev.p3", 53 }, 54 }, 55 }, 56 { 57 Name: "role/dev.all", 58 Includes: []string{ 59 "role/dev.a", 60 "role/dev.b", 61 }, 62 }, 63 { 64 Name: "role/dev.unused", 65 Permissions: []*protocol.Permission{ 66 { 67 Name: "luci.dev.p2", 68 }, 69 { 70 Name: "luci.dev.p3", 71 }, 72 { 73 Name: "luci.dev.p4", 74 }, 75 { 76 Name: "luci.dev.p5", 77 }, 78 { 79 Name: "luci.dev.unused", 80 }, 81 }, 82 }, 83 { 84 Name: "role/implicitRoot", 85 Permissions: []*protocol.Permission{ 86 { 87 Name: "luci.dev.implicitRoot", 88 }, 89 }, 90 }, 91 }, 92 Attribute: []string{"a1", "a2", "root"}, 93 }, &config.Meta{ 94 Path: "permissions.cfg", 95 Revision: "123", 96 }) 97 db.ImplicitRootBindings = func(s string) []*realmsconf.Binding { return nil } 98 if implicitRootBindings { 99 db.ImplicitRootBindings = func(projectID string) []*realmsconf.Binding { 100 return []*realmsconf.Binding{ 101 { 102 Role: "role/implicitRoot", 103 Principals: []string{fmt.Sprintf("project:%s", projectID)}, 104 }, 105 { 106 Role: "role/implicitRoot", 107 Principals: []string{"group:root"}, 108 Conditions: []*realmsconf.Condition{ 109 { 110 Op: &realmsconf.Condition_Restrict{ 111 Restrict: &realmsconf.Condition_AttributeRestriction{ 112 Attribute: "root", 113 Values: []string{"yes"}, 114 }, 115 }, 116 }, 117 }, 118 }, 119 } 120 } 121 } 122 return db 123 } 124 func TestConditionsSet(t *testing.T) { 125 t.Parallel() 126 restriction := func(attr string, values []string) *realmsconf.Condition { 127 return &realmsconf.Condition{ 128 Op: &realmsconf.Condition_Restrict{ 129 Restrict: &realmsconf.Condition_AttributeRestriction{ 130 Attribute: attr, 131 Values: values, 132 }, 133 }, 134 } 135 } 136 Convey("test key", t, func() { 137 cond1 := &protocol.Condition{ 138 Op: &protocol.Condition_Restrict{ 139 Restrict: &protocol.Condition_AttributeRestriction{ 140 Attribute: "attr", 141 Values: []string{"test"}, 142 }, 143 }, 144 } 145 cond2 := &protocol.Condition{ 146 Op: &protocol.Condition_Restrict{ 147 Restrict: &protocol.Condition_AttributeRestriction{ 148 Attribute: "attr", 149 Values: []string{"test"}, 150 }, 151 }, 152 } 153 // same contents == same key 154 cond1Key, cond2Key := conditionKey(cond1), conditionKey(cond2) 155 So(cond1Key, ShouldEqual, cond2Key) 156 condEmpty := &protocol.Condition{} 157 So(conditionKey(condEmpty), ShouldEqual, "") 158 }) 159 Convey("errors", t, func() { 160 cs := &ConditionsSet{ 161 normalized: map[string]*conditionMapTuple{}, 162 indexMapping: map[*realmsconf.Condition]uint32{}, 163 } 164 r1 := restriction("a", []string{"1", "2"}) 165 r2 := restriction("b", []string{"1"}) 166 So(cs.addCond(r1), ShouldBeNil) 167 cs.finalize() 168 So(cs.addCond(r2), ShouldEqual, ErrFinalized) 169 }) 170 Convey("works", t, func() { 171 cs := &ConditionsSet{ 172 normalized: map[string]*conditionMapTuple{}, 173 indexMapping: map[*realmsconf.Condition]uint32{}, 174 finalized: false, 175 } 176 r1 := restriction("b", []string{"1", "2"}) 177 r2 := restriction("a", []string{"2", "1", "1"}) 178 r3 := restriction("a", []string{"1", "2"}) 179 r4 := restriction("a", []string{"3", "4"}) 180 So(cs.addCond(r1), ShouldBeNil) 181 So(cs.addCond(r1), ShouldBeNil) 182 So(cs.addCond(r2), ShouldBeNil) 183 So(cs.addCond(r3), ShouldBeNil) 184 So(cs.addCond(r4), ShouldBeNil) 185 out := cs.finalize() 186 expected := []*protocol.Condition{ 187 { 188 Op: &protocol.Condition_Restrict{ 189 Restrict: &protocol.Condition_AttributeRestriction{ 190 Attribute: "a", 191 Values: []string{"1", "2"}, 192 }, 193 }, 194 }, 195 { 196 Op: &protocol.Condition_Restrict{ 197 Restrict: &protocol.Condition_AttributeRestriction{ 198 Attribute: "a", 199 Values: []string{"3", "4"}, 200 }, 201 }, 202 }, 203 { 204 Op: &protocol.Condition_Restrict{ 205 Restrict: &protocol.Condition_AttributeRestriction{ 206 Attribute: "b", 207 Values: []string{"1", "2"}, 208 }, 209 }, 210 }, 211 } 212 So(out, ShouldResembleProto, expected) 213 So(cs.indexes([]*realmsconf.Condition{r1}), ShouldResemble, []uint32{2}) 214 So(cs.indexes([]*realmsconf.Condition{r2}), ShouldResemble, []uint32{0}) 215 So(cs.indexes([]*realmsconf.Condition{r3}), ShouldResemble, []uint32{0}) 216 So(cs.indexes([]*realmsconf.Condition{r4}), ShouldResemble, []uint32{1}) 217 inds := cs.indexes([]*realmsconf.Condition{r1, r2, r3, r4}) 218 So(inds, ShouldResemble, []uint32{0, 1, 2}) 219 }) 220 } 221 func TestRolesExpander(t *testing.T) { 222 t.Parallel() 223 Convey("errors", t, func() { 224 permDB := testPermissionsDB(false) 225 r := &RolesExpander{ 226 builtinRoles: permDB.Roles, 227 customRoles: map[string]*realmsconf.CustomRole{}, 228 permissions: map[string]uint32{}, 229 roles: map[string]*indexSet{}, 230 } 231 _, err := r.role("role/notbuiltin") 232 So(err, ShouldErrLike, ErrRoleNotFound) 233 _, err = r.role("customRole/notarole") 234 So(err, ShouldErrLike, ErrRoleNotFound) 235 _, err = r.role("notarole/test") 236 So(err, ShouldErrLike, ErrImpossibleRole) 237 }) 238 Convey("test builtin roles works", t, func() { 239 permDB := testPermissionsDB(false) 240 r := &RolesExpander{ 241 builtinRoles: permDB.Roles, 242 permissions: map[string]uint32{}, 243 roles: map[string]*indexSet{}, 244 } 245 actual, err := r.role("role/dev.a") 246 So(err, ShouldBeNil) 247 So(actual, ShouldResemble, IndexSetFromSlice([]uint32{0, 1})) 248 actual, err = r.role("role/dev.b") 249 So(err, ShouldBeNil) 250 So(actual, ShouldResemble, IndexSetFromSlice([]uint32{1, 2})) 251 perms, mapping := r.sortedPermissions() 252 So(perms, ShouldResemble, []string{"luci.dev.p1", "luci.dev.p2", "luci.dev.p3"}) 253 So(mapping, ShouldResemble, []uint32{0, 1, 2}) 254 }) 255 Convey("test custom roles works", t, func() { 256 permDB := testPermissionsDB(false) 257 r := &RolesExpander{ 258 builtinRoles: permDB.Roles, 259 customRoles: map[string]*realmsconf.CustomRole{ 260 "customRole/custom1": { 261 Name: "customRole/custom1", 262 Extends: []string{"role/dev.a", "customRole/custom2", "customRole/custom3"}, 263 Permissions: []string{"luci.dev.p1", "luci.dev.p4"}, 264 }, 265 "customRole/custom2": { 266 Name: "customRole/custom2", 267 Extends: []string{"customRole/custom3"}, 268 Permissions: []string{"luci.dev.p4"}, 269 }, 270 "customRole/custom3": { 271 Name: "customRole/custom3", 272 Extends: []string{"role/dev.b"}, 273 Permissions: []string{"luci.dev.p5"}, 274 }, 275 }, 276 permissions: map[string]uint32{}, 277 roles: map[string]*indexSet{}, 278 } 279 actual, err := r.role("customRole/custom1") 280 So(err, ShouldBeNil) 281 So(actual, ShouldResemble, IndexSetFromSlice([]uint32{0, 1, 2, 3, 4})) 282 actual, err = r.role("customRole/custom2") 283 So(err, ShouldBeNil) 284 So(actual, ShouldResemble, IndexSetFromSlice([]uint32{1, 2, 3, 4})) 285 actual, err = r.role("customRole/custom3") 286 So(err, ShouldBeNil) 287 So(actual, ShouldResemble, IndexSetFromSlice([]uint32{2, 3, 4})) 288 perms, mapping := r.sortedPermissions() 289 So(perms, ShouldResemble, []string{"luci.dev.p1", "luci.dev.p2", "luci.dev.p3", "luci.dev.p4", "luci.dev.p5"}) 290 So(mapping, ShouldResemble, []uint32{0, 3, 1, 4, 2}) 291 reMap := func(perms []string, mapping []uint32, permSet []uint32) []string { 292 res := make([]string, 0, len(permSet)) 293 for _, idx := range permSet { 294 res = append(res, perms[mapping[idx]]) 295 } 296 return res 297 } 298 // This test is a bit redundant but just to ensure the permissions since 299 // eyeballing the numbers is difficult. 300 permSet, err := r.role("customRole/custom1") 301 So(err, ShouldBeNil) 302 So(reMap(perms, mapping, permSet.toSortedSlice()), ShouldResemble, []string{ 303 "luci.dev.p1", 304 "luci.dev.p4", 305 "luci.dev.p2", 306 "luci.dev.p5", 307 "luci.dev.p3", 308 }) 309 permSet, err = r.role("customRole/custom2") 310 So(err, ShouldBeNil) 311 So(reMap(perms, mapping, permSet.toSortedSlice()), ShouldResemble, []string{ 312 "luci.dev.p4", 313 "luci.dev.p2", 314 "luci.dev.p5", 315 "luci.dev.p3", 316 }) 317 permSet, err = r.role("customRole/custom3") 318 So(err, ShouldBeNil) 319 So(reMap(perms, mapping, permSet.toSortedSlice()), ShouldResemble, []string{ 320 "luci.dev.p2", 321 "luci.dev.p5", 322 "luci.dev.p3", 323 }) 324 }) 325 } 326 327 func TestRealmsExpander(t *testing.T) { 328 t.Parallel() 329 330 Convey("test perPrincipalBindings", t, func() { 331 Convey("errors", func() { 332 Convey("realm not found", func() { 333 r := &RealmsExpander{} 334 _, err := r.perPrincipalBindings("test") 335 So(err, ShouldErrLike, "realm test not found in RealmsExpander") 336 }) 337 338 Convey("parent not found", func() { 339 r := &RealmsExpander{ 340 realms: map[string]*realmsconf.Realm{ 341 "test": { 342 Name: "test", 343 Extends: []string{"test-2"}, 344 }, 345 }, 346 } 347 348 _, err := r.perPrincipalBindings("test") 349 So(err, ShouldErrLike, "failed when getting parent bindings") 350 }) 351 352 Convey("realm name mismatch", func() { 353 r := &RealmsExpander{ 354 realms: map[string]*realmsconf.Realm{ 355 "test": { 356 Name: "not-test", 357 }, 358 }, 359 } 360 _, err := r.perPrincipalBindings("test") 361 So(err, ShouldErrLike, "given realm: test does not match name found internally: not-test") 362 }) 363 364 Convey("permissions fetch issue (ErrRoleNotfound)", func() { 365 r := &RealmsExpander{ 366 rolesExpander: &RolesExpander{ 367 permissions: map[string]uint32{}, 368 builtinRoles: map[string]*permissions.Role{}, 369 roles: map[string]*indexSet{}, 370 }, 371 realms: map[string]*realmsconf.Realm{ 372 "@root": { 373 Name: "@root", 374 }, 375 "test": { 376 Name: "test", 377 Extends: []string{}, 378 Bindings: []*realmsconf.Binding{ 379 { 380 Role: "role/test-role", 381 Principals: []string{"test-project"}, 382 }, 383 }, 384 EnforceInService: []string{}, 385 }, 386 }, 387 } 388 _, err := r.perPrincipalBindings("test") 389 So(err, ShouldErrLike, "there was an issue fetching permissions") 390 }) 391 }) 392 }) 393 }