go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tokenserver/appengine/impl/delegation/config_test.go (about)

     1  // Copyright 2016 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 delegation
    16  
    17  import (
    18  	"context"
    19  	"testing"
    20  
    21  	"google.golang.org/protobuf/encoding/prototext"
    22  
    23  	"go.chromium.org/luci/auth/identity"
    24  	"go.chromium.org/luci/server/auth"
    25  	"go.chromium.org/luci/server/auth/authtest"
    26  	admin "go.chromium.org/luci/tokenserver/api/admin/v1"
    27  	"go.chromium.org/luci/tokenserver/appengine/impl/utils/identityset"
    28  	"go.chromium.org/luci/tokenserver/appengine/impl/utils/policy"
    29  
    30  	. "github.com/smartystreets/goconvey/convey"
    31  	. "go.chromium.org/luci/common/testing/assertions"
    32  )
    33  
    34  func TestIsAuthorizedRequestor(t *testing.T) {
    35  	t.Parallel()
    36  
    37  	ctx := auth.WithState(context.Background(), &authtest.FakeState{
    38  		Identity: "user:some-user@example.com",
    39  	})
    40  
    41  	Convey("IsAuthorizedRequestor works", t, func() {
    42  		cfg, err := loadConfig(ctx, `
    43  			rules {
    44  				name: "rule 1"
    45  				requestor: "user:some-user@example.com"
    46  
    47  				target_service: "service:some-service"
    48  				allowed_to_impersonate: "group:some-group"
    49  				allowed_audience: "REQUESTOR"
    50  				max_validity_duration: 86400
    51  			}
    52  
    53  			rules {
    54  				name: "rule 2"
    55  				requestor: "user:some-another-user@example.com"
    56  				requestor: "group:some-group"
    57  
    58  				target_service: "service:some-service"
    59  				allowed_to_impersonate: "group:some-group"
    60  				allowed_audience: "REQUESTOR"
    61  				max_validity_duration: 86400
    62  			}
    63  		`)
    64  		So(err, ShouldBeNil)
    65  		So(cfg, ShouldNotBeNil)
    66  
    67  		res, err := cfg.IsAuthorizedRequestor(ctx, identity.Identity("user:some-user@example.com"))
    68  		So(err, ShouldBeNil)
    69  		So(res, ShouldBeTrue)
    70  
    71  		ctx = auth.WithState(context.Background(), &authtest.FakeState{
    72  			Identity: "user:some-another-user@example.com",
    73  		})
    74  		res, err = cfg.IsAuthorizedRequestor(ctx, identity.Identity("user:some-another-user@example.com"))
    75  		So(err, ShouldBeNil)
    76  		So(res, ShouldBeTrue)
    77  
    78  		ctx = auth.WithState(context.Background(), &authtest.FakeState{
    79  			Identity: "user:unknown-user@example.com",
    80  		})
    81  		res, err = cfg.IsAuthorizedRequestor(ctx, identity.Identity("user:unknown-user@example.com"))
    82  		So(err, ShouldBeNil)
    83  		So(res, ShouldBeFalse)
    84  
    85  		ctx = auth.WithState(context.Background(), &authtest.FakeState{
    86  			Identity:       "user:via-group@example.com",
    87  			IdentityGroups: []string{"some-group"},
    88  		})
    89  		res, err = cfg.IsAuthorizedRequestor(ctx, identity.Identity("user:via-group@example.com"))
    90  		So(err, ShouldBeNil)
    91  		So(res, ShouldBeTrue)
    92  	})
    93  }
    94  
    95  func TestFindMatchingRule(t *testing.T) {
    96  	t.Parallel()
    97  
    98  	ctx := auth.WithState(context.Background(), &authtest.FakeState{
    99  		Identity: "user:requestor@example.com",
   100  		FakeDB: authtest.NewFakeDB(
   101  			authtest.MockMembership("user:requestor-group-member@example.com", "requestor-group"),
   102  			authtest.MockMembership("user:delegators-group-member@example.com", "delegators-group"),
   103  			authtest.MockMembership("user:audience-group-member@example.com", "audience-group"),
   104  			authtest.MockMembership("user:luci-service@example.com", "auth-luci-services"),
   105  		),
   106  	})
   107  
   108  	Convey("with example config", t, func() {
   109  		cfg, err := loadConfig(ctx, `
   110  			rules {
   111  				name: "rule 1"
   112  				requestor: "user:requestor@example.com"
   113  				target_service: "service:some-service"
   114  				allowed_to_impersonate: "user:allowed-to-impersonate@example.com"
   115  				allowed_audience: "user:allowed-audience@example.com"
   116  				max_validity_duration: 86400
   117  			}
   118  
   119  			rules {
   120  				name: "rule 2"
   121  				requestor: "group:requestor-group"
   122  				target_service: "service:some-service"
   123  				allowed_to_impersonate: "group:delegators-group"
   124  				allowed_audience: "group:audience-group"
   125  				max_validity_duration: 86400
   126  			}
   127  
   128  			rules {
   129  				name: "rule 3"
   130  				requestor: "group:requestor-group"
   131  				target_service: "service:some-service"
   132  				allowed_to_impersonate: "REQUESTOR"
   133  				allowed_audience: "REQUESTOR"
   134  				max_validity_duration: 86400
   135  			}
   136  
   137  			rules {
   138  				name: "rule 4"
   139  				requestor: "user:some-requestor@example.com"
   140  				requestor: "user:conflicts-with-rule-5@example.com"
   141  				target_service: "*"
   142  				allowed_to_impersonate: "REQUESTOR"
   143  				allowed_audience: "*"
   144  				max_validity_duration: 86400
   145  			}
   146  
   147  			rules {
   148  				name: "rule 5"
   149  				requestor: "user:conflicts-with-rule-5@example.com"
   150  				target_service: "*"
   151  				allowed_to_impersonate: "REQUESTOR"
   152  				allowed_audience: "*"
   153  				max_validity_duration: 86400
   154  			}
   155  		`)
   156  		So(err, ShouldBeNil)
   157  		So(cfg, ShouldNotBeNil)
   158  
   159  		Convey("Direct matches and misses", func() {
   160  			// Match.
   161  			res, err := cfg.FindMatchingRule(ctx, &RulesQuery{
   162  				Requestor: "user:requestor@example.com",
   163  				Delegator: "user:allowed-to-impersonate@example.com",
   164  				Audience:  makeSet("user:allowed-audience@example.com"),
   165  				Services:  makeSet("service:some-service"),
   166  			})
   167  			So(err, ShouldBeNil)
   168  			So(res, ShouldNotBeNil)
   169  			So(res.Name, ShouldEqual, "rule 1")
   170  
   171  			// Unknown requestor.
   172  			res, err = cfg.FindMatchingRule(ctx, &RulesQuery{
   173  				Requestor: "user:unknown-requestor@example.com",
   174  				Delegator: "user:allowed-to-impersonate@example.com",
   175  				Audience:  makeSet("user:allowed-audience@example.com"),
   176  				Services:  makeSet("service:some-service"),
   177  			})
   178  			So(err, ShouldErrLike, "no matching delegation rules in the config")
   179  			So(res, ShouldBeNil)
   180  
   181  			// Unknown delegator.
   182  			res, err = cfg.FindMatchingRule(ctx, &RulesQuery{
   183  				Requestor: "user:requestor@example.com",
   184  				Delegator: "user:unknown-allowed-to-impersonate@example.com",
   185  				Audience:  makeSet("user:allowed-audience@example.com"),
   186  				Services:  makeSet("service:some-service"),
   187  			})
   188  			So(err, ShouldErrLike, "no matching delegation rules in the config")
   189  			So(res, ShouldBeNil)
   190  
   191  			// Unknown audience.
   192  			res, err = cfg.FindMatchingRule(ctx, &RulesQuery{
   193  				Requestor: "user:requestor@example.com",
   194  				Delegator: "user:allowed-to-impersonate@example.com",
   195  				Audience:  makeSet("user:unknown-allowed-audience@example.com"),
   196  				Services:  makeSet("service:some-service"),
   197  			})
   198  			So(err, ShouldErrLike, "no matching delegation rules in the config")
   199  			So(res, ShouldBeNil)
   200  
   201  			// Unknown target service.
   202  			res, err = cfg.FindMatchingRule(ctx, &RulesQuery{
   203  				Requestor: "user:requestor@example.com",
   204  				Delegator: "user:allowed-to-impersonate@example.com",
   205  				Audience:  makeSet("user:allowed-audience@example.com"),
   206  				Services:  makeSet("service:unknown-some-service"),
   207  			})
   208  			So(err, ShouldErrLike, "no matching delegation rules in the config")
   209  			So(res, ShouldBeNil)
   210  		})
   211  
   212  		Convey("Matches via groups", func() {
   213  			res, err := cfg.FindMatchingRule(ctx, &RulesQuery{
   214  				Requestor: "user:requestor-group-member@example.com",
   215  				Delegator: "user:delegators-group-member@example.com",
   216  				Audience:  makeSet("group:audience-group"),
   217  				Services:  makeSet("service:some-service"),
   218  			})
   219  			So(err, ShouldBeNil)
   220  			So(res, ShouldNotBeNil)
   221  			So(res.Name, ShouldEqual, "rule 2")
   222  
   223  			// Doesn't do group lookup when checking audience!
   224  			res, err = cfg.FindMatchingRule(ctx, &RulesQuery{
   225  				Requestor: "user:requestor-group-member@example.com",
   226  				Delegator: "user:delegators-group-member@example.com",
   227  				Audience:  makeSet("user:audience-group-member@example.com"),
   228  				Services:  makeSet("service:some-service"),
   229  			})
   230  			So(err, ShouldErrLike, "no matching delegation rules in the config")
   231  			So(res, ShouldBeNil)
   232  		})
   233  
   234  		Convey("REQUESTOR rules work", func() {
   235  			res, err := cfg.FindMatchingRule(ctx, &RulesQuery{
   236  				Requestor: "user:requestor-group-member@example.com",
   237  				Delegator: "user:requestor-group-member@example.com",
   238  				Audience:  makeSet("user:requestor-group-member@example.com"),
   239  				Services:  makeSet("service:some-service"),
   240  			})
   241  			So(err, ShouldBeNil)
   242  			So(res, ShouldNotBeNil)
   243  			So(res.Name, ShouldEqual, "rule 3")
   244  		})
   245  
   246  		Convey("'*' rules work", func() {
   247  			res, err := cfg.FindMatchingRule(ctx, &RulesQuery{
   248  				Requestor: "user:some-requestor@example.com",
   249  				Delegator: "user:some-requestor@example.com",
   250  				Audience:  makeSet("group:abc", "user:def@example.com"),
   251  				Services:  makeSet("service:unknown"),
   252  			})
   253  			So(err, ShouldBeNil)
   254  			So(res, ShouldNotBeNil)
   255  			So(res.Name, ShouldEqual, "rule 4")
   256  		})
   257  
   258  		Convey("a conflict is handled", func() {
   259  			res, err := cfg.FindMatchingRule(ctx, &RulesQuery{
   260  				Requestor: "user:conflicts-with-rule-5@example.com",
   261  				Delegator: "user:conflicts-with-rule-5@example.com",
   262  				Audience:  makeSet("group:abc", "user:def@example.com"),
   263  				Services:  makeSet("service:unknown"),
   264  			})
   265  			So(err, ShouldErrLike, `ambiguous request, multiple delegation rules match ("rule 4", "rule 5")`)
   266  			So(res, ShouldBeNil)
   267  		})
   268  
   269  		Convey("implicit project:* rule works", func() {
   270  			res, err := cfg.FindMatchingRule(ctx, &RulesQuery{
   271  				Requestor: "user:luci-service@example.com",
   272  				Delegator: "project:some-project",
   273  				Audience:  makeSet("user:luci-service@example.com"),
   274  				Services:  makeSet("service:some-target-service"),
   275  			})
   276  			So(err, ShouldBeNil)
   277  			So(res, ShouldNotBeNil)
   278  			So(res.Name, ShouldEqual, "allow-project-identities")
   279  		})
   280  	})
   281  }
   282  
   283  func loadConfig(ctx context.Context, text string) (*Rules, error) {
   284  	cfg := &admin.DelegationPermissions{}
   285  	err := prototext.Unmarshal([]byte(text), cfg)
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  	rules, err := prepareRules(ctx, policy.ConfigBundle{delegationCfg: cfg}, "fake-revision")
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  	return rules.(*Rules), nil
   294  }
   295  
   296  func makeSet(ident ...string) *identityset.Set {
   297  	s, err := identityset.FromStrings(ident, nil)
   298  	if err != nil {
   299  		panic(err)
   300  	}
   301  	return s
   302  }