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  }