blob: fa380b7c88b4dadc0702a6440aca2f368c6a0df4 [file] [log] [blame]
// Copyright 2016 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"strings"
"time"
"golang.org/x/net/context"
"golang.org/x/oauth2"
"google.golang.org/grpc/credentials"
"github.com/luci/luci-go/common/auth"
"github.com/luci/luci-go/common/logging"
"github.com/luci/luci-go/server/auth/delegation"
"github.com/luci/luci-go/server/auth/identity"
"github.com/luci/luci-go/server/auth/internal"
)
// RPCAuthorityKind defines under whose authority RPCs are made.
type RPCAuthorityKind int
const (
// NoAuth is used for outbound RPCs that don't have any implicit auth headers.
NoAuth RPCAuthorityKind = iota
// AsSelf is used for outbound RPCs sent with the authority of the current
// service itself.
//
// RPC requests done in this mode will have 'Authorization' header set to the
// OAuth2 access token of the service's own service account.
//
// By default uses "https://www.googleapis.com/auth/userinfo.email" API scope.
// Can be customized with WithScopes() options.
AsSelf
// AsUser is used for outbound RPCs that inherit the authority of a user
// that initiated the request that is currently being handled.
//
// If the current request was initiated by an anonymous caller, the RPC will
// have no auth headers (just like in NoAuth mode).
//
// Can also be used together with MintDelegationToken to make requests on
// user behalf asynchronously. For example, to associate end-user authority
// with some delayed task, call MintDelegationToken (in a context of a user
// initiated request) when this task is created and store the resulting token
// along with the task. Then, to make an RPC on behalf of the user from the
// task use GetRPCTransport(ctx, AsUser, WithDelegationToken(token)).
//
// The implementation is based on LUCI-specific protocol that uses special
// delegation tokens. Only LUCI backends can understand them.
AsUser
// AsActor is used for outbound RPCs sent with the authority of some service
// account that the current service has "iam.serviceAccountActor" role in.
//
// RPC requests done in this mode will have 'Authorization' header set to
// the access token of the service account specified by WithServiceAccount()
// option.
//
// By default uses "https://www.googleapis.com/auth/userinfo.email" API scope.
// Can be customized with WithScopes() options.
AsActor
)
// RPCOption is an option for GetRPCTransport or GetPerRPCCredentials functions.
type RPCOption interface {
apply(opts *rpcOptions)
}
// WithScopes can be used to customize OAuth scopes for outbound RPC requests.
//
// If not used, the requests are made with "userinfo.email" scope.
func WithScopes(scopes ...string) RPCOption {
return oauthScopesOption{scopes: scopes}
}
type oauthScopesOption struct {
scopes []string
}
func (o oauthScopesOption) apply(opts *rpcOptions) {
opts.scopes = append(opts.scopes, o.scopes...)
}
// WithServiceAccount option must be used with AsActor authority kind to specify
// what service account to act as.
func WithServiceAccount(email string) RPCOption {
return serviceAccountOption{email: email}
}
type serviceAccountOption struct {
email string
}
func (o serviceAccountOption) apply(opts *rpcOptions) {
opts.serviceAccount = o.email
}
// WithDelegationToken can be used to attach an existing delegation token to
// requests made in AsUser mode.
//
// The token can be obtained earlier via MintDelegationToken call. The transport
// doesn't attempt to validate it and just blindly sends it to the other side.
func WithDelegationToken(token string) RPCOption {
return delegationTokenOption{token: token}
}
type delegationTokenOption struct {
token string
}
func (o delegationTokenOption) apply(opts *rpcOptions) {
opts.delegationToken = o.token
}
// GetRPCTransport returns http.RoundTripper to use for outbound HTTP RPC
// requests.
//
// Usage:
// tr, err := auth.GetRPCTransport(c, auth.AsSelf, auth.WithScopes("..."))
// if err != nil {
// return err
// }
// client := &http.Client{Transport: tr}
// ...
func GetRPCTransport(c context.Context, kind RPCAuthorityKind, opts ...RPCOption) (http.RoundTripper, error) {
options, err := makeRPCOptions(kind, opts)
if err != nil {
return nil, err
}
config := getConfig(c)
if config == nil || config.AnonymousTransport == nil {
return nil, ErrNotConfigured
}
baseTransport := config.AnonymousTransport(c)
if options.kind == NoAuth {
return baseTransport, nil
}
return auth.NewModifyingTransport(baseTransport, func(req *http.Request) error {
tok, extra, err := options.getRPCHeaders(c, req.URL.String(), options)
if err != nil {
return err
}
if tok != nil {
req.Header.Set("Authorization", tok.TokenType+" "+tok.AccessToken)
}
for k, v := range extra {
req.Header.Set(k, v)
}
return nil
}), nil
}
// GetPerRPCCredentials returns gRPC's PerRPCCredentials implementation.
//
// It can be used to authenticate outbound gPRC RPC's.
func GetPerRPCCredentials(kind RPCAuthorityKind, opts ...RPCOption) (credentials.PerRPCCredentials, error) {
options, err := makeRPCOptions(kind, opts)
if err != nil {
return nil, err
}
return perRPCCreds{options}, nil
}
type perRPCCreds struct {
options *rpcOptions
}
func (creds perRPCCreds) GetRequestMetadata(c context.Context, uri ...string) (map[string]string, error) {
if len(uri) == 0 {
panic("perRPCCreds: no URI given")
}
tok, extra, err := creds.options.getRPCHeaders(c, uri[0], creds.options)
switch {
case err != nil:
return nil, err
case tok == nil && len(extra) == 0:
return nil, nil
}
headers := make(map[string]string, 1+len(extra))
if tok != nil {
headers["Authorization"] = tok.TokenType + " " + tok.AccessToken
}
for k, v := range extra {
headers[k] = v
}
return headers, nil
}
func (creds perRPCCreds) RequireTransportSecurity() bool {
return true
}
// GetTokenSource returns an oauth2.TokenSource bound to the supplied Context.
//
// Supports only AsSelf and AsActor authorization kinds, since they are only
// ones that exclusively use OAuth2 tokens and no other extra headers.
//
// While GetPerRPCCredentials is preferred, this can be used by packages that
// cannot or do not properly handle this gRPC option.
func GetTokenSource(c context.Context, kind RPCAuthorityKind, opts ...RPCOption) (oauth2.TokenSource, error) {
if kind != AsSelf && kind != AsActor {
return nil, fmt.Errorf("auth: GetTokenSource can only be used with AsSelf or AsActor authorization kind")
}
options, err := makeRPCOptions(kind, opts)
if err != nil {
return nil, err
}
return &tokenSource{c, options}, nil
}
type tokenSource struct {
ctx context.Context
options *rpcOptions
}
func (ts *tokenSource) Token() (*oauth2.Token, error) {
tok, extra, err := ts.options.getRPCHeaders(ts.ctx, "", ts.options)
switch {
case err != nil:
return nil, err
case tok == nil:
panic("oauth2.Token is unexpectedly nil")
case extra != nil:
keys := make([]string, 0, len(extra))
for k := range extra {
keys = append(keys, k)
}
panic(fmt.Errorf("extra headers are unexpectedly not empty: %s", keys))
}
return tok, nil
}
////////////////////////////////////////////////////////////////////////////////
// Internal stuff.
func init() {
// This is needed to allow packages imported by 'server/auth' to make
// authenticated calls. They can't use GetRPCTransport directly, since they
// can't import 'server/auth' (it creates an import cycle).
internal.RegisterClientFactory(func(c context.Context, scopes []string) (*http.Client, error) {
var t http.RoundTripper
var err error
if len(scopes) == 0 {
t, err = GetRPCTransport(c, NoAuth)
} else {
t, err = GetRPCTransport(c, AsSelf, WithScopes(scopes...))
}
if err != nil {
return nil, err
}
return &http.Client{Transport: t}, nil
})
}
// tokenFingerprint returns first 16 bytes of SHA256 of the token, as hex.
//
// Token fingerprints can be used to identify tokens without parsing them.
func tokenFingerprint(tok string) string {
digest := sha256.Sum256([]byte(tok))
return hex.EncodeToString(digest[:16])
}
// rpcMocks are used exclusively in unit tests.
type rpcMocks struct {
MintDelegationToken func(context.Context, DelegationTokenParams) (*delegation.Token, error)
MintAccessTokenForServiceAccount func(context.Context, MintAccessTokenParams) (*oauth2.Token, error)
}
// apply implements RPCOption interface.
func (o *rpcMocks) apply(opts *rpcOptions) {
opts.rpcMocks = o
}
var defaultOAuthScopes = []string{auth.OAuthScopeEmail}
// headersGetter returns a main Authorization token and optional additional
// headers.
type headersGetter func(c context.Context, uri string, opts *rpcOptions) (*oauth2.Token, map[string]string, error)
type rpcOptions struct {
kind RPCAuthorityKind
scopes []string // for AsSelf and AsActor
serviceAccount string // for AsActor
delegationToken string // for AsUser
getRPCHeaders headersGetter
rpcMocks *rpcMocks
}
// makeRPCOptions applies all options and validates them.
func makeRPCOptions(kind RPCAuthorityKind, opts []RPCOption) (*rpcOptions, error) {
options := &rpcOptions{kind: kind}
for _, o := range opts {
o.apply(options)
}
// Set default scopes.
asSelfOrActor := options.kind == AsSelf || options.kind == AsActor
if asSelfOrActor && len(options.scopes) == 0 {
options.scopes = defaultOAuthScopes
}
// Validate options.
if !asSelfOrActor && len(options.scopes) != 0 {
return nil, fmt.Errorf("auth: WithScopes can only be used with AsSelf or AsActor authorization kind")
}
if options.serviceAccount != "" && options.kind != AsActor {
return nil, fmt.Errorf("auth: WithServiceAccount can only be used with AsActor authorization kind")
}
if options.serviceAccount == "" && options.kind == AsActor {
return nil, fmt.Errorf("auth: AsActor authorization kind requires WithServiceAccount option")
}
if options.delegationToken != "" && options.kind != AsUser {
return nil, fmt.Errorf("auth: WithDelegationToken can only be used with AsUser authorization kind")
}
// Validate 'kind' and pick correct implementation of getRPCHeaders.
switch options.kind {
case NoAuth:
options.getRPCHeaders = noAuthHeaders
case AsSelf:
options.getRPCHeaders = asSelfHeaders
case AsUser:
options.getRPCHeaders = asUserHeaders
case AsActor:
options.getRPCHeaders = asActorHeaders
default:
return nil, fmt.Errorf("auth: unknown RPCAuthorityKind %d", options.kind)
}
return options, nil
}
// noAuthHeaders is getRPCHeaders for NoAuth mode.
func noAuthHeaders(c context.Context, uri string, opts *rpcOptions) (*oauth2.Token, map[string]string, error) {
return nil, nil, nil
}
// asSelfHeaders returns a map of authentication headers to add to outbound
// RPC requests done in AsSelf mode.
//
// This will be called by the transport layer on each request.
func asSelfHeaders(c context.Context, uri string, opts *rpcOptions) (*oauth2.Token, map[string]string, error) {
cfg := getConfig(c)
if cfg == nil || cfg.AccessTokenProvider == nil {
return nil, nil, ErrNotConfigured
}
tok, err := cfg.AccessTokenProvider(c, opts.scopes)
return tok, nil, err
}
// asUserHeaders returns a map of authentication headers to add to outbound
// RPC requests done in AsUser mode.
//
// This will be called by the transport layer on each request.
func asUserHeaders(c context.Context, uri string, opts *rpcOptions) (*oauth2.Token, map[string]string, error) {
cfg := getConfig(c)
if cfg == nil || cfg.AccessTokenProvider == nil {
return nil, nil, ErrNotConfigured
}
delegationToken := ""
if opts.delegationToken != "" {
delegationToken = opts.delegationToken // WithDelegationToken was used
} else {
// Outbound RPC calls in the context of a request from anonymous caller are
// anonymous too. No need to use any authentication headers.
userIdent := CurrentIdentity(c)
if userIdent == identity.AnonymousIdentity {
return nil, nil, nil
}
// Grab root URL of the destination service. Only https:// are allowed.
uri = strings.ToLower(uri)
if !strings.HasPrefix(uri, "https://") {
return nil, nil, fmt.Errorf("auth: refusing to use delegation tokens with non-https URL")
}
host := uri[len("https://"):]
if idx := strings.IndexRune(host, '/'); idx != -1 {
host = host[:idx]
}
// Grab a token that's good enough for at least 10 min. Outbound RPCs
// shouldn't last longer than that.
mintTokenCall := MintDelegationToken
if opts.rpcMocks != nil && opts.rpcMocks.MintDelegationToken != nil {
mintTokenCall = opts.rpcMocks.MintDelegationToken
}
tok, err := mintTokenCall(c, DelegationTokenParams{
TargetHost: host,
MinTTL: 10 * time.Minute,
})
if err != nil {
return nil, nil, err
}
delegationToken = tok.Token
}
// Use our own OAuth token too, since the delegation token is bound to us.
oauthTok, err := cfg.AccessTokenProvider(c, []string{auth.OAuthScopeEmail})
if err != nil {
return nil, nil, err
}
logging.Fields{
"fingerprint": tokenFingerprint(delegationToken),
}.Debugf(c, "auth: Sending delegation token")
return oauthTok, map[string]string{delegation.HTTPHeaderName: delegationToken}, nil
}
// asActorHeaders returns a map of authentication headers to add to outbound
// RPC requests done in AsActor mode.
//
// This will be called by the transport layer on each request.
func asActorHeaders(c context.Context, uri string, opts *rpcOptions) (*oauth2.Token, map[string]string, error) {
mintTokenCall := MintAccessTokenForServiceAccount
if opts.rpcMocks != nil && opts.rpcMocks.MintAccessTokenForServiceAccount != nil {
mintTokenCall = opts.rpcMocks.MintAccessTokenForServiceAccount
}
oauthTok, err := mintTokenCall(c, MintAccessTokenParams{
ServiceAccount: opts.serviceAccount,
Scopes: opts.scopes,
MinTTL: 2 * time.Minute,
})
return oauthTok, nil, err
}