go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tokenserver/appengine/impl/delegation/rpc_mint_delegation_token_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  	"fmt"
    20  	"net"
    21  	"net/url"
    22  	"testing"
    23  	"time"
    24  
    25  	"go.opentelemetry.io/otel/trace"
    26  
    27  	"go.chromium.org/luci/appengine/gaetesting"
    28  	"go.chromium.org/luci/auth/identity"
    29  	"go.chromium.org/luci/common/clock/testclock"
    30  	"go.chromium.org/luci/server/auth"
    31  	"go.chromium.org/luci/server/auth/authdb"
    32  	"go.chromium.org/luci/server/auth/authtest"
    33  	"go.chromium.org/luci/server/auth/signing"
    34  	"go.chromium.org/luci/server/auth/signing/signingtest"
    35  	"go.chromium.org/luci/tokenserver/api/admin/v1"
    36  	"go.chromium.org/luci/tokenserver/api/minter/v1"
    37  
    38  	. "github.com/smartystreets/goconvey/convey"
    39  	. "go.chromium.org/luci/common/testing/assertions"
    40  )
    41  
    42  var testingRequestID = trace.TraceID{1, 2, 3, 4, 5}
    43  
    44  func mockedFetchLUCIServiceIdentity(c context.Context, u string) (identity.Identity, error) {
    45  	l, err := url.Parse(u)
    46  	if err != nil {
    47  		return "", err
    48  	}
    49  	if l.Scheme != "https" {
    50  		return "", fmt.Errorf("wrong scheme")
    51  	}
    52  	if l.Host == "crash" {
    53  		return "", fmt.Errorf("boom")
    54  	}
    55  	return identity.MakeIdentity("service:" + l.Host)
    56  }
    57  
    58  func init() {
    59  	fetchLUCIServiceIdentity = mockedFetchLUCIServiceIdentity
    60  }
    61  
    62  func testingContext() context.Context {
    63  	ctx := gaetesting.TestingContext()
    64  	ctx = trace.ContextWithSpanContext(ctx, trace.NewSpanContext(trace.SpanContextConfig{
    65  		TraceID: testingRequestID,
    66  	}))
    67  	ctx, _ = testclock.UseTime(ctx, time.Date(2015, time.February, 3, 4, 5, 6, 0, time.UTC))
    68  	return auth.WithState(ctx, &authtest.FakeState{
    69  		Identity:       "user:requestor@example.com",
    70  		PeerIPOverride: net.ParseIP("127.10.10.10"),
    71  		FakeDB:         &authdb.SnapshotDB{Rev: 1234},
    72  	})
    73  }
    74  
    75  func testingSigner() *signingtest.Signer {
    76  	return signingtest.NewSigner(&signing.ServiceInfo{
    77  		ServiceAccountName: "signer@testing.host",
    78  		AppID:              "unit-tests",
    79  		AppVersion:         "mocked-ver",
    80  	})
    81  }
    82  
    83  func TestBuildRulesQuery(t *testing.T) {
    84  	t.Parallel()
    85  
    86  	ctx := testingContext()
    87  
    88  	Convey("Happy path", t, func() {
    89  		q, err := buildRulesQuery(ctx, &minter.MintDelegationTokenRequest{
    90  			DelegatedIdentity: "user:delegated@example.com",
    91  			Audience:          []string{"group:A", "group:B", "user:c@example.com"},
    92  			Services:          []string{"service:A", "*"},
    93  		}, "user:requestor@example.com")
    94  		So(err, ShouldBeNil)
    95  		So(q, ShouldNotBeNil)
    96  
    97  		So(q.Requestor, ShouldEqual, identity.Identity("user:requestor@example.com"))
    98  		So(q.Delegator, ShouldEqual, identity.Identity("user:delegated@example.com"))
    99  		So(q.Audience.ToStrings(), ShouldResemble, []string{"group:A", "group:B", "user:c@example.com"})
   100  		So(q.Services.ToStrings(), ShouldResemble, []string{"*"})
   101  	})
   102  
   103  	Convey("REQUESTOR usage works", t, func() {
   104  		q, err := buildRulesQuery(ctx, &minter.MintDelegationTokenRequest{
   105  			DelegatedIdentity: "REQUESTOR",
   106  			Audience:          []string{"group:A", "group:B", "REQUESTOR"},
   107  			Services:          []string{"*"},
   108  		}, "user:requestor@example.com")
   109  		So(err, ShouldBeNil)
   110  		So(q, ShouldNotBeNil)
   111  
   112  		So(q.Requestor, ShouldEqual, identity.Identity("user:requestor@example.com"))
   113  		So(q.Delegator, ShouldEqual, identity.Identity("user:requestor@example.com"))
   114  		So(q.Audience.ToStrings(), ShouldResemble, []string{"group:A", "group:B", "user:requestor@example.com"})
   115  	})
   116  
   117  	Convey("bad 'delegated_identity'", t, func() {
   118  		_, err := buildRulesQuery(ctx, &minter.MintDelegationTokenRequest{
   119  			Audience: []string{"REQUESTOR"},
   120  			Services: []string{"*"},
   121  		}, "user:requestor@example.com")
   122  		So(err, ShouldErrLike, `'delegated_identity' is required`)
   123  
   124  		_, err = buildRulesQuery(ctx, &minter.MintDelegationTokenRequest{
   125  			DelegatedIdentity: "junk",
   126  			Audience:          []string{"REQUESTOR"},
   127  			Services:          []string{"*"},
   128  		}, "user:requestor@example.com")
   129  		So(err, ShouldErrLike, `bad 'delegated_identity' - auth: bad identity string "junk"`)
   130  	})
   131  
   132  	Convey("bad 'audience'", t, func() {
   133  		_, err := buildRulesQuery(ctx, &minter.MintDelegationTokenRequest{
   134  			DelegatedIdentity: "REQUESTOR",
   135  			Services:          []string{"*"},
   136  		}, "user:requestor@example.com")
   137  		So(err, ShouldErrLike, `'audience' is required`)
   138  
   139  		_, err = buildRulesQuery(ctx, &minter.MintDelegationTokenRequest{
   140  			DelegatedIdentity: "REQUESTOR",
   141  			Audience:          []string{"REQUESTOR", "junk"},
   142  			Services:          []string{"*"},
   143  		}, "user:requestor@example.com")
   144  		So(err, ShouldErrLike, `bad 'audience' - auth: bad identity string "junk"`)
   145  	})
   146  
   147  	Convey("bad 'services'", t, func() {
   148  		_, err := buildRulesQuery(ctx, &minter.MintDelegationTokenRequest{
   149  			DelegatedIdentity: "REQUESTOR",
   150  			Audience:          []string{"REQUESTOR"},
   151  		}, "user:requestor@example.com")
   152  		So(err, ShouldErrLike, `'services' is required`)
   153  
   154  		_, err = buildRulesQuery(ctx, &minter.MintDelegationTokenRequest{
   155  			DelegatedIdentity: "REQUESTOR",
   156  			Audience:          []string{"REQUESTOR"},
   157  			Services:          []string{"junk"},
   158  		}, "user:requestor@example.com")
   159  		So(err, ShouldErrLike, `bad 'services' - auth: bad identity string "junk"`)
   160  
   161  		_, err = buildRulesQuery(ctx, &minter.MintDelegationTokenRequest{
   162  			DelegatedIdentity: "REQUESTOR",
   163  			Audience:          []string{"REQUESTOR"},
   164  			Services:          []string{"user:abc@example.com"},
   165  		}, "user:requestor@example.com")
   166  		So(err, ShouldErrLike, `bad 'services' - "user:abc@example.com" is not a service ID`)
   167  
   168  		_, err = buildRulesQuery(ctx, &minter.MintDelegationTokenRequest{
   169  			DelegatedIdentity: "REQUESTOR",
   170  			Audience:          []string{"REQUESTOR"},
   171  			Services:          []string{"group:abc"},
   172  		}, "user:requestor@example.com")
   173  		So(err, ShouldErrLike, `bad 'services' - can't specify groups`)
   174  	})
   175  
   176  	Convey("resolves https:// service refs", t, func() {
   177  		q, err := buildRulesQuery(ctx, &minter.MintDelegationTokenRequest{
   178  			DelegatedIdentity: "user:delegated@example.com",
   179  			Audience:          []string{"*"},
   180  			Services: []string{
   181  				"service:A",
   182  				"service:B",
   183  				"https://C",
   184  				"https://B",
   185  				"https://A",
   186  			},
   187  		}, "user:requestor@example.com")
   188  		So(err, ShouldBeNil)
   189  		So(q, ShouldNotBeNil)
   190  
   191  		So(q.Services.ToStrings(), ShouldResemble, []string{
   192  			"service:A",
   193  			"service:B",
   194  			"service:C",
   195  		})
   196  	})
   197  
   198  	Convey("handles errors when resolving https:// service refs", t, func() {
   199  		_, err := buildRulesQuery(ctx, &minter.MintDelegationTokenRequest{
   200  			DelegatedIdentity: "user:delegated@example.com",
   201  			Audience:          []string{"*"},
   202  			Services: []string{
   203  				"https://A",
   204  				"https://B",
   205  				"https://crash",
   206  			},
   207  		}, "user:requestor@example.com")
   208  		So(err, ShouldErrLike, `could not resolve "https://crash" to service ID - boom`)
   209  	})
   210  }
   211  
   212  func TestMintDelegationToken(t *testing.T) {
   213  	t.Parallel()
   214  
   215  	ctx := testingContext()
   216  
   217  	Convey("with mocked config and state", t, func() {
   218  		cfg, err := loadConfig(ctx, `
   219  			rules {
   220  				name: "requstor for itself"
   221  				requestor: "user:requestor@example.com"
   222  				target_service: "*"
   223  				allowed_to_impersonate: "REQUESTOR"
   224  				allowed_audience: "REQUESTOR"
   225  				max_validity_duration: 3600
   226  			}
   227  		`)
   228  		So(err, ShouldBeNil)
   229  
   230  		mintMock := func(c context.Context, p *mintParams) (*minter.MintDelegationTokenResponse, error) {
   231  			return &minter.MintDelegationTokenResponse{Token: "valid_token", ServiceVersion: p.serviceVer}, nil
   232  		}
   233  
   234  		var loggedInfo *MintedTokenInfo
   235  		rpc := MintDelegationTokenRPC{
   236  			Signer: testingSigner(),
   237  			Rules:  func(context.Context) (*Rules, error) { return cfg, nil },
   238  			LogToken: func(c context.Context, i *MintedTokenInfo) error {
   239  				loggedInfo = i
   240  				return nil
   241  			},
   242  			mintMock: mintMock,
   243  		}
   244  
   245  		Convey("Happy path", func() {
   246  			req := &minter.MintDelegationTokenRequest{
   247  				DelegatedIdentity: "REQUESTOR",
   248  				Audience:          []string{"REQUESTOR"},
   249  				Services:          []string{"*"},
   250  				Tags:              []string{"k:v"},
   251  			}
   252  			resp, err := rpc.MintDelegationToken(ctx, req)
   253  			So(err, ShouldBeNil)
   254  			So(resp.Token, ShouldEqual, "valid_token")
   255  			So(resp.ServiceVersion, ShouldEqual, "unit-tests/mocked-ver")
   256  
   257  			// LogToken called.
   258  			So(loggedInfo.Request, ShouldResembleProto, req)
   259  			So(loggedInfo.Response, ShouldResembleProto, resp)
   260  			So(loggedInfo.ConfigRev, ShouldEqual, cfg.ConfigRevision())
   261  			So(loggedInfo.Rule, ShouldResembleProto, &admin.DelegationRule{
   262  				Name:                 "requstor for itself",
   263  				Requestor:            []string{"user:requestor@example.com"},
   264  				AllowedToImpersonate: []string{"REQUESTOR"},
   265  				AllowedAudience:      []string{"REQUESTOR"},
   266  				TargetService:        []string{"*"},
   267  				MaxValidityDuration:  3600,
   268  			})
   269  			So(loggedInfo.PeerIP, ShouldResemble, net.ParseIP("127.10.10.10"))
   270  			So(loggedInfo.RequestID, ShouldEqual, testingRequestID.String())
   271  			So(loggedInfo.AuthDBRev, ShouldEqual, 1234)
   272  		})
   273  
   274  		Convey("Using delegated identity for auth is forbidden", func() {
   275  			ctx := auth.WithState(ctx, &authtest.FakeState{
   276  				Identity:             "user:requestor@example.com",
   277  				PeerIdentityOverride: "user:impersonator@example.com",
   278  			})
   279  			_, err := rpc.MintDelegationToken(ctx, &minter.MintDelegationTokenRequest{
   280  				DelegatedIdentity: "REQUESTOR",
   281  				Audience:          []string{"REQUESTOR"},
   282  				Services:          []string{"*"},
   283  			})
   284  			So(err, ShouldBeRPCPermissionDenied, "delegation is forbidden for this API call")
   285  		})
   286  
   287  		Convey("Anonymous calls are forbidden", func() {
   288  			ctx := auth.WithState(ctx, &authtest.FakeState{
   289  				Identity: "anonymous:anonymous",
   290  			})
   291  			_, err := rpc.MintDelegationToken(ctx, &minter.MintDelegationTokenRequest{
   292  				DelegatedIdentity: "REQUESTOR",
   293  				Audience:          []string{"REQUESTOR"},
   294  				Services:          []string{"*"},
   295  			})
   296  			So(err, ShouldBeRPCUnauthenticated, "authentication required")
   297  		})
   298  
   299  		Convey("Unauthorized requestor", func() {
   300  			ctx := auth.WithState(ctx, &authtest.FakeState{
   301  				Identity: "user:unknown@example.com",
   302  			})
   303  			_, err := rpc.MintDelegationToken(ctx, &minter.MintDelegationTokenRequest{
   304  				DelegatedIdentity: "REQUESTOR",
   305  				Audience:          []string{"REQUESTOR"},
   306  				Services:          []string{"*"},
   307  			})
   308  			So(err, ShouldBeRPCPermissionDenied, "not authorized")
   309  		})
   310  
   311  		Convey("Negative validity duration", func() {
   312  			_, err := rpc.MintDelegationToken(ctx, &minter.MintDelegationTokenRequest{
   313  				DelegatedIdentity: "REQUESTOR",
   314  				Audience:          []string{"REQUESTOR"},
   315  				Services:          []string{"*"},
   316  				ValidityDuration:  -1,
   317  			})
   318  			So(err, ShouldBeRPCInvalidArgument, "bad request - invalid 'validity_duration' (-1)")
   319  		})
   320  
   321  		Convey("Bad tags", func() {
   322  			_, err := rpc.MintDelegationToken(ctx, &minter.MintDelegationTokenRequest{
   323  				DelegatedIdentity: "REQUESTOR",
   324  				Audience:          []string{"REQUESTOR"},
   325  				Services:          []string{"*"},
   326  				Tags:              []string{"not key value"},
   327  			})
   328  			So(err, ShouldBeRPCInvalidArgument, "bad request - invalid 'tags': tag #1: not in <key>:<value> form")
   329  		})
   330  
   331  		Convey("Malformed request", func() {
   332  			_, err := rpc.MintDelegationToken(ctx, &minter.MintDelegationTokenRequest{
   333  				DelegatedIdentity: "REQUESTOR",
   334  				Audience:          []string{"junk"},
   335  				Services:          []string{"*"},
   336  			})
   337  			So(err, ShouldBeRPCInvalidArgument, `bad request - bad 'audience' - auth: bad identity string "junk"`)
   338  		})
   339  
   340  		Convey("No matching rules", func() {
   341  			_, err := rpc.MintDelegationToken(ctx, &minter.MintDelegationTokenRequest{
   342  				DelegatedIdentity: "REQUESTOR",
   343  				Audience:          []string{"user:someone-else@example.com"},
   344  				Services:          []string{"*"},
   345  			})
   346  			So(err, ShouldBeRPCPermissionDenied, "forbidden - no matching delegation rules in the config")
   347  		})
   348  
   349  		Convey("Forbidden validity duration", func() {
   350  			_, err := rpc.MintDelegationToken(ctx, &minter.MintDelegationTokenRequest{
   351  				DelegatedIdentity: "REQUESTOR",
   352  				Audience:          []string{"REQUESTOR"},
   353  				Services:          []string{"*"},
   354  				ValidityDuration:  3601,
   355  			})
   356  			So(err, ShouldBeRPCPermissionDenied, "forbidden - the requested validity duration (3601 sec) exceeds the maximum allowed one (3600 sec)")
   357  		})
   358  
   359  	})
   360  }