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  }