go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/authdb/internal/realmset/realmset_test.go (about) 1 // Copyright 2020 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 realmset 16 17 import ( 18 "context" 19 "sort" 20 "testing" 21 22 "go.chromium.org/luci/server/auth/authdb/internal/graph" 23 "go.chromium.org/luci/server/auth/realms" 24 "go.chromium.org/luci/server/auth/service/protocol" 25 26 . "github.com/smartystreets/goconvey/convey" 27 ) 28 29 var ( 30 permTesting0 = realms.RegisterPermission("luci.dev.testing0") 31 permTesting1 = realms.RegisterPermission("luci.dev.testing1") 32 permTesting2 = realms.RegisterPermission("luci.dev.testing2") 33 permUnknown = realms.RegisterPermission("luci.dev.unknown") 34 permIgnored = realms.RegisterPermission("luci.dev.ignored") 35 ) 36 37 func init() { 38 permTesting0.AddFlags(realms.UsedInQueryRealms) 39 permTesting1.AddFlags(realms.UsedInQueryRealms) 40 } 41 42 func TestRealms(t *testing.T) { 43 t.Parallel() 44 45 ctx := context.Background() 46 47 grp := groups(map[string][]string{ 48 "g1": {}, 49 "g2": {}, 50 }) 51 52 // Kick out permIgnored to test what happens to "dynamically" registered 53 // permissions. Note that we avoid really dynamically registering it because 54 // the registry lives in the global process memory and dynamically mutating 55 // it in t.Parallel() test is flaky. 56 registered := realms.RegisteredPermissions() 57 delete(registered, permIgnored) 58 59 Convey("Works", t, func() { 60 r, err := Build(&protocol.Realms{ 61 ApiVersion: ExpectedAPIVersion, 62 Permissions: []*protocol.Permission{ 63 {Name: "luci.dev.testing0"}, 64 {Name: "luci.dev.testing1"}, 65 {Name: "luci.dev.testing2"}, 66 {Name: "luci.dev.ignored"}, 67 }, 68 Realms: []*protocol.Realm{ 69 { 70 Name: "proj:r1", 71 Bindings: []*protocol.Binding{ 72 { 73 Permissions: []uint32{0, 3}, 74 Principals: []string{ 75 "group:g1", 76 "group:unknown", 77 "user:u1@example.com", 78 }, 79 }, 80 { 81 Permissions: []uint32{0, 1, 2}, 82 Principals: []string{ 83 "group:g1", 84 "user:u2@example.com", 85 }, 86 }, 87 { 88 Permissions: []uint32{2, 3}, 89 Principals: []string{"group:g2", "user:u2@example.com"}, 90 }, 91 }, 92 Data: &protocol.RealmData{ 93 EnforceInService: []string{"a"}, 94 }, 95 }, 96 { 97 Name: "proj:r2", 98 Bindings: []*protocol.Binding{ 99 { 100 Permissions: []uint32{0}, 101 Principals: []string{ 102 "group:g1", 103 }, 104 }, 105 }, 106 }, 107 { 108 Name: "another:r1", 109 Bindings: []*protocol.Binding{ 110 { 111 Permissions: []uint32{0, 1, 2}, 112 Principals: []string{ 113 "group:g1", 114 }, 115 }, 116 }, 117 }, 118 { 119 Name: "proj:empty", 120 Bindings: []*protocol.Binding{ 121 { 122 Permissions: []uint32{0}, 123 }, 124 { 125 Permissions: []uint32{0, 1, 2}, 126 }, 127 }, 128 }, 129 { 130 Name: "proj:only-ignored", 131 Bindings: []*protocol.Binding{ 132 { 133 Permissions: []uint32{3}, 134 }, 135 }, 136 }, 137 }, 138 }, grp, registered) 139 So(err, ShouldBeNil) 140 141 So(r.perms, ShouldResemble, map[string]PermissionIndex{ 142 "luci.dev.testing0": 0, 143 "luci.dev.testing1": 1, 144 "luci.dev.testing2": 2, 145 "luci.dev.ignored": 3, 146 }) 147 So(r.names.ToSortedSlice(), ShouldResemble, []string{ 148 "another:r1", 149 "proj:empty", 150 "proj:only-ignored", 151 "proj:r1", 152 "proj:r2", 153 }) 154 155 idx, ok := r.PermissionIndex(permTesting2) 156 So(ok, ShouldBeTrue) 157 So(idx, ShouldEqual, 2) 158 159 _, ok = r.PermissionIndex(permUnknown) 160 So(ok, ShouldBeFalse) 161 162 So(r.HasRealm("proj:r1"), ShouldBeTrue) 163 So(r.HasRealm("proj:empty"), ShouldBeTrue) 164 So(r.HasRealm("proj:unknown"), ShouldBeFalse) 165 So(r.HasRealm("proj:only-ignored"), ShouldBeTrue) 166 167 So(r.Data("proj:r1").EnforceInService, ShouldResemble, []string{"a"}) 168 So(r.Data("proj:empty"), ShouldBeNil) 169 So(r.Data("proj:unknown"), ShouldBeNil) 170 171 bs := r.Bindings("proj:r1", 0) 172 So(bs, ShouldHaveLength, 1) 173 So(bs[0].Groups, ShouldResemble, indexes(grp, "g1")) 174 So(bs[0].Idents.ToSortedSlice(), ShouldResemble, []string{"user:u1@example.com", "user:u2@example.com"}) 175 176 bs = r.Bindings("proj:r1", 1) 177 So(bs, ShouldHaveLength, 1) 178 So(bs[0].Groups, ShouldResemble, indexes(grp, "g1")) 179 So(bs[0].Idents.ToSortedSlice(), ShouldResemble, []string{"user:u2@example.com"}) 180 181 bs = r.Bindings("proj:r1", 2) 182 So(bs, ShouldHaveLength, 1) 183 So(bs[0].Groups, ShouldResemble, indexes(grp, "g1", "g2")) 184 So(bs[0].Idents.ToSortedSlice(), ShouldResemble, []string{"user:u2@example.com"}) 185 186 So(r.Bindings("proj:empty", 0), ShouldBeEmpty) 187 So(r.Bindings("proj:unknown", 0), ShouldBeEmpty) 188 189 // This isn't really happening in real programs since they are not usually 190 // registering permissions dynamically after building Realms set, but check 191 // that such "late" permissions are basically ignored. 192 idx, _ = r.PermissionIndex(permIgnored) 193 So(idx, ShouldEqual, 3) 194 So(r.Bindings("proj:r1", 3), ShouldBeEmpty) 195 196 // Check bindings from QueryBindings match what Bindings(...) returns and 197 // also convert the result into a map we can easily pass to ShouldResemble. 198 checkBindingsMap := func(m map[string][]RealmBindings, perm PermissionIndex) map[string][]string { 199 out := map[string][]string{} 200 for proj, realms := range m { 201 for _, realmAndBindings := range realms { 202 So(realmAndBindings.Bindings, ShouldResemble, r.Bindings(realmAndBindings.Realm, perm)) 203 out[proj] = append(out[proj], realmAndBindings.Realm) 204 } 205 sort.Strings(out[proj]) 206 } 207 return out 208 } 209 210 bindings, ok := r.QueryBindings(0) 211 So(ok, ShouldBeTrue) 212 So(checkBindingsMap(bindings, 0), ShouldResemble, map[string][]string{ 213 "another": {"another:r1"}, 214 "proj": {"proj:r1", "proj:r2"}, 215 }) 216 217 bindings, ok = r.QueryBindings(1) 218 So(ok, ShouldBeTrue) 219 So(checkBindingsMap(bindings, 1), ShouldResemble, map[string][]string{ 220 "another": {"another:r1"}, 221 "proj": {"proj:r1"}, 222 }) 223 224 // The permission is not flagged with UsedInQueryRealms. 225 _, ok = r.QueryBindings(2) 226 So(ok, ShouldBeFalse) 227 }) 228 229 Convey("Conditional bindings", t, func() { 230 r, err := Build(&protocol.Realms{ 231 ApiVersion: ExpectedAPIVersion, 232 Permissions: []*protocol.Permission{ 233 {Name: "luci.dev.testing0"}, 234 {Name: "luci.dev.testing1"}, 235 {Name: "luci.dev.ignored"}, 236 }, 237 Conditions: []*protocol.Condition{ 238 restrict("a", "ok"), 239 restrict("b", "ok"), 240 }, 241 Realms: []*protocol.Realm{ 242 { 243 Name: "proj:r1", 244 Bindings: []*protocol.Binding{ 245 { 246 Permissions: []uint32{0, 2}, 247 Principals: []string{ 248 "user:0@example.com", 249 }, 250 }, 251 { 252 Permissions: []uint32{1, 2}, 253 Principals: []string{ 254 "user:1@example.com", 255 }, 256 }, 257 { 258 Permissions: []uint32{0, 2}, 259 Conditions: []uint32{0}, 260 Principals: []string{ 261 "user:0-if-0@example.com", 262 }, 263 }, 264 { 265 Permissions: []uint32{0}, 266 Conditions: []uint32{1}, 267 Principals: []string{ 268 "user:0-if-1@example.com", 269 }, 270 }, 271 { 272 Permissions: []uint32{0, 1}, 273 Principals: []string{ 274 "user:01@example.com", 275 }, 276 }, 277 { 278 Permissions: []uint32{0, 1}, 279 Conditions: []uint32{0}, 280 Principals: []string{ 281 "user:01-if-0@example.com", 282 }, 283 }, 284 { 285 Permissions: []uint32{0, 1}, 286 Conditions: []uint32{1}, 287 Principals: []string{ 288 "user:01-if-1@example.com", 289 }, 290 }, 291 { 292 Permissions: []uint32{0}, 293 Conditions: []uint32{0, 1}, 294 Principals: []string{ 295 "user:0-if-0&1@example.com", 296 }, 297 }, 298 { 299 Permissions: []uint32{1, 2}, 300 Conditions: []uint32{0, 1}, 301 Principals: []string{ 302 "user:1-if-0&1@example.com", 303 }, 304 }, 305 { 306 Permissions: []uint32{1}, 307 Conditions: []uint32{1}, 308 Principals: []string{ 309 "user:1-if-1@example.com", 310 }, 311 }, 312 }, 313 }, 314 }, 315 }, grp, registered) 316 So(err, ShouldBeNil) 317 318 type pretty struct { 319 cond int // index of the condition+1 or 0 if unconditional 320 users []string 321 } 322 323 prettify := func(bs Bindings) []pretty { 324 out := make([]pretty, len(bs)) 325 for i, b := range bs { 326 cond := 0 327 if b.Condition != nil { 328 cond = b.Condition.Index() + 1 329 } 330 out[i] = pretty{ 331 cond: cond, 332 users: b.Idents.ToSortedSlice(), 333 } 334 } 335 return out 336 } 337 338 bs0 := r.Bindings("proj:r1", 0) 339 So(prettify(bs0), ShouldResemble, []pretty{ 340 {cond: 0, users: []string{"user:01@example.com", "user:0@example.com"}}, 341 {cond: 1, users: []string{"user:0-if-0@example.com", "user:01-if-0@example.com"}}, 342 {cond: 2, users: []string{"user:0-if-1@example.com", "user:01-if-1@example.com"}}, 343 {cond: 3, users: []string{"user:0-if-0&1@example.com"}}, 344 }) 345 346 bs1 := r.Bindings("proj:r1", 1) 347 So(prettify(bs1), ShouldResemble, []pretty{ 348 {cond: 0, users: []string{"user:01@example.com", "user:1@example.com"}}, 349 {cond: 1, users: []string{"user:01-if-0@example.com"}}, 350 {cond: 2, users: []string{"user:01-if-1@example.com", "user:1-if-1@example.com"}}, 351 {cond: 3, users: []string{"user:1-if-0&1@example.com"}}, 352 }) 353 354 // The "non-active" permission is ignored. 355 So(r.Bindings("proj:r1", 2), ShouldBeEmpty) 356 357 // Now actually confirm mapping of `cond` indexes above to elementary 358 // conditions from Realms proto. 359 360 // 1 is elementary 0: attr.a==ok. 361 cond1 := bs0[1].Condition 362 So(cond1.Index(), ShouldEqual, 0) 363 So(cond1.Eval(ctx, realms.Attrs{"a": "ok"}), ShouldBeTrue) 364 So(cond1.Eval(ctx, realms.Attrs{"a": "??"}), ShouldBeFalse) 365 366 // 2 is elementary 1: attr.b==ok. 367 cond2 := bs0[2].Condition 368 So(cond2.Index(), ShouldEqual, 1) 369 So(cond2.Eval(ctx, realms.Attrs{"b": "ok"}), ShouldBeTrue) 370 So(cond2.Eval(ctx, realms.Attrs{"b": "??"}), ShouldBeFalse) 371 372 // 3 is elementary 0&1: attr.a==ok && attr.b==ok. 373 cond3 := bs0[3].Condition 374 So(cond3.Index(), ShouldEqual, 2) 375 So(cond3.Eval(ctx, realms.Attrs{"a": "ok", "b": "ok"}), ShouldBeTrue) 376 So(cond3.Eval(ctx, realms.Attrs{"a": "??", "b": "ok"}), ShouldBeFalse) 377 So(cond3.Eval(ctx, realms.Attrs{"a": "ok", "b": "??"}), ShouldBeFalse) 378 }) 379 } 380 381 func groups(gr map[string][]string) *graph.QueryableGraph { 382 g := make([]*protocol.AuthGroup, 0, len(gr)) 383 for name, members := range gr { 384 g = append(g, &protocol.AuthGroup{ 385 Name: name, 386 Members: members, 387 }) 388 } 389 q, err := graph.BuildQueryable(g) 390 if err != nil { 391 panic(err) 392 } 393 return q 394 } 395 396 func indexes(q *graph.QueryableGraph, groups ...string) graph.SortedNodeSet { 397 ns := graph.NodeSet{} 398 for _, g := range groups { 399 idx, ok := q.GroupIndex(g) 400 if !ok { 401 panic("unknown group " + g) 402 } 403 ns.Add(idx) 404 } 405 return ns.Sort() 406 } 407 408 func restrict(attr, val string) *protocol.Condition { 409 return &protocol.Condition{ 410 Op: &protocol.Condition_Restrict{ 411 Restrict: &protocol.Condition_AttributeRestriction{ 412 Attribute: attr, 413 Values: []string{val}, 414 }, 415 }, 416 } 417 }