github.com/grailbio/base@v0.0.11/cmd/grail-access/main.go (about)

     1  // Copyright 2018 GRAIL, Inc. All rights reserved.
     2  // Use of this source code is governed by the Apache-2.0
     3  // license that can be found in the LICENSE file.
     4  
     5  // The following enables go generate to generate the doc.go file.
     6  //go:generate go run v.io/x/lib/cmdline/gendoc "--build-cmd=go install" --copyright-notice= . -help
     7  
     8  package main
     9  
    10  import (
    11  	"fmt"
    12  	"os"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/grailbio/base/cmd/grail-access/remote"
    17  	"github.com/grailbio/base/errors"
    18  	"github.com/grailbio/base/log"
    19  	_ "github.com/grailbio/v23/factories/grail"
    20  	v23 "v.io/v23"
    21  	"v.io/v23/context"
    22  	"v.io/v23/security"
    23  	"v.io/x/lib/cmdline"
    24  	"v.io/x/ref"
    25  	libsecurity "v.io/x/ref/lib/security"
    26  )
    27  
    28  const (
    29  	// DATA(sensitive): These are the OAuth2 client ID and secret. They were
    30  	// generated in the grail-razvanm Google Cloud Project. The client secret
    31  	// is not secret in this case because it is part of client tool. It does act
    32  	// as an identifier that allows restriction based on quota on the Google
    33  	// side.
    34  	clientID     = "fake"
    35  	clientSecret = "fake"
    36  )
    37  
    38  var (
    39  	credentialsDirFlag string
    40  
    41  	blesserFlag             string
    42  	browserFlag             bool
    43  	googleOauth2Flag        string
    44  	ec2Flag                 bool
    45  	ec2InstanceIdentityFlag string
    46  	k8sFlag                 bool
    47  	regionFlag              string
    48  	caCrtFlag               string
    49  	namespaceFlag           string
    50  	tokenFlag               string
    51  
    52  	dumpFlag                 bool
    53  	doNotRefreshDurationFlag time.Duration
    54  	expiryCaveatFlag         string
    55  
    56  	blessRemotesFlag        bool
    57  	blessRemotesModeFlag    string
    58  	blessRemotesTargetsFlag FlagStrings
    59  )
    60  
    61  func init() {
    62  	blessRemotesTargetsFlag = []string{os.ExpandEnv("ec2-name:ubuntu@adhoc.${USER}.*")}
    63  }
    64  
    65  func main() {
    66  	var defaultCredentialsDir string
    67  	if dir, ok := os.LookupEnv(ref.EnvCredentials); ok {
    68  		defaultCredentialsDir = dir
    69  	} else {
    70  		// TODO(josh): This expands to /.v23 if $HOME is undefined.
    71  		// We keep this for backwards compatibility, but maybe we shouldn't.
    72  		defaultCredentialsDir = os.ExpandEnv("${HOME}/.v23")
    73  	}
    74  
    75  	cmd := &cmdline.Command{
    76  		Runner: cmdline.RunnerFunc(run),
    77  		Name:   "grail-access",
    78  		Short:  "Creates fresh Vanadium credentials",
    79  		Long: `
    80  Command grail-access creates Vanadium credentials (also called principals) using
    81  either Google ID tokens (the default) or the AWS IAM role attached to an EC2
    82  instance (requested using the '-ec2' flag).
    83  
    84  For the Google-based auth the user will be prompted to go through an
    85  OAuth flow that requires minimal permissions (only 'Know who you are
    86  on Google') and obtains an ID token scoped to the clientID expected by
    87  the server. The ID token is presented to the server via a Vanadium
    88  RPC. For a 'xxx@grailbio.com' email address the server will hand to
    89  the client a '[server]:google:xxx@grailbio.com' blessing where
    90  '[server]' is the blessing of the server.
    91  
    92  For the EC2-based auth an instance with ID 'i-0aec7b085f8432699' in the account
    93  number '619867110810' using the 'adhoc' role the server will hand to the client
    94  a '[server]:ec2:619867110810:role:adhoc:i-0aec7b085f8432699' blessing where
    95  'server' is the blessing of the server.
    96  `,
    97  	}
    98  	cmd.Flags.StringVar(&credentialsDirFlag, "dir", defaultCredentialsDir, "Where to store the Vanadium credentials. NOTE: the content will be erased if the credentials are regenerated.")
    99  	cmd.Flags.StringVar(&blesserFlag, "blesser", "", "Flow specific blesser endpoint to use. Defaults to /ticket-server.eng.grail.com:8102/blesser/<flow>.")
   100  	cmd.Flags.BoolVar(&browserFlag, "browser", os.Getenv("SSH_CLIENT") == "", "Attempt to open a browser.")
   101  	cmd.Flags.StringVar(&googleOauth2Flag, "google-oauth2-url",
   102  		"https://accounts.google.com/o/oauth2",
   103  		"URL for oauth2 API calls, for testing")
   104  	cmd.Flags.BoolVar(&ec2Flag, "ec2", false, "Use the role of the EC2 VM.")
   105  	cmd.Flags.StringVar(&ec2InstanceIdentityFlag, "ec2-instance-identity-url",
   106  		"http://169.254.169.254/latest/dynamic/instance-identity/pkcs7",
   107  		"URL for fetching instance identity document, for testing")
   108  	cmd.Flags.BoolVar(&k8sFlag, "k8s", false, "Use the Kubernetes flow.")
   109  	cmd.Flags.StringVar(&regionFlag, "region", "us-west-2", "AWS EKS region to use for k8s cluster token review.")
   110  	cmd.Flags.StringVar(&caCrtFlag, "ca-crt", "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", "Path to ca.crt file.")
   111  	cmd.Flags.StringVar(&namespaceFlag, "namespace", "/var/run/secrets/kubernetes.io/serviceaccount/namespace", "Path to namespace file.")
   112  	cmd.Flags.StringVar(&tokenFlag, "token", "/var/run/secrets/kubernetes.io/serviceaccount/token", "Path to token file.")
   113  	cmd.Flags.BoolVar(&dumpFlag, "dump", false, "If credentials are present, dump them on the console instead of refreshing them.")
   114  	cmd.Flags.DurationVar(&doNotRefreshDurationFlag, "do-not-refresh-duration", 7*24*time.Hour, "Do not refresh credentials if they are present and do not expire within this duration.")
   115  	cmd.Flags.StringVar(&expiryCaveatFlag, "expiry-caveat", "", "Duration of expiry caveat added to blessings (for testing); empty means no caveat added")
   116  
   117  	// TODO(2022-10-18): Fix commentary generation to bring doc.go up to date.
   118  	// go.mod is currently broken such that required go tooling fails.  We are
   119  	// apparently specifying old versions of protobuf related packages, which
   120  	// causes `go install` to fail, which causes doc generation to fail.
   121  	cmd.Flags.BoolVar(&blessRemotesFlag, "bless-remotes", true, "Whether to attempt to bless remotes with local blessings; only applies to Google blessings")
   122  	cmd.Flags.StringVar(&blessRemotesModeFlag, remote.FlagNameMode, "", "(INTERNAL) Controls the mode in which we run for the remote blessing protocol; one of {public-key,receive,send}")
   123  	cmd.Flags.Var(&blessRemotesTargetsFlag, "bless-remotes-targets", "Comma-separated list of targets to bless; targets may be \"ssh:[user@]host[:port]\" SSH destinations or \"ec2-name:[user@]ec2-instance-name-filter\" EC2 instance name filters; see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Filtering.html")
   124  
   125  	cmdline.HideGlobalFlagsExcept()
   126  	cmdline.Main(cmd)
   127  }
   128  
   129  func run(*cmdline.Env, []string) error {
   130  	if credentialsDirFlag == "" {
   131  		return fmt.Errorf("missing credentials dir, need -dir, $HOME, or $%s", ref.EnvCredentials)
   132  	}
   133  
   134  	if _, ok := os.LookupEnv(ref.EnvCredentials); !ok {
   135  		fmt.Print("*******************************************************\n")
   136  		fmt.Printf("*    WARNING: $%s is not defined!        *\n", ref.EnvCredentials)
   137  		fmt.Printf("*******************************************************\n\n")
   138  		fmt.Printf("How to fix this in bash: export %s=%s\n\n", ref.EnvCredentials, credentialsDirFlag)
   139  	}
   140  	principal, err := libsecurity.LoadPersistentPrincipal(credentialsDirFlag, nil)
   141  	if err != nil {
   142  		log.Printf("INFO: Couldn't load principal from %s. Creating new one...", credentialsDirFlag)
   143  		_, createErr := libsecurity.CreatePersistentPrincipal(credentialsDirFlag, nil)
   144  		if createErr != nil {
   145  			return errors.E(fmt.Sprintf("failed to create new principal: %v, after load error: %v", createErr, err))
   146  		}
   147  		principal, err = libsecurity.LoadPersistentPrincipal(credentialsDirFlag, nil)
   148  	}
   149  	if err != nil {
   150  		return errors.E("failed to load principal", err)
   151  	}
   152  
   153  	ctx, shutDown := v23.Init()
   154  	defer shutDown()
   155  	ctx, err = v23.WithPrincipal(ctx, principal)
   156  	if err != nil {
   157  		return errors.E("failed to initialize context", err)
   158  	}
   159  	switch blessRemotesModeFlag {
   160  	case "":
   161  		// No-op.
   162  	case remote.ModePublicKey:
   163  		if err = remote.PrintPublicKey(ctx, os.Stdout); err != nil {
   164  			return errors.E("failed to print public key", err)
   165  		}
   166  		return nil
   167  	case remote.ModeReceive:
   168  		if err = remote.ReceiveBlessings(ctx, os.Stdin); err != nil {
   169  			return errors.E("failed to receive blessings", err)
   170  		}
   171  		return nil
   172  	default:
   173  		return errors.E("invalid -"+remote.FlagNameMode, blessRemotesModeFlag)
   174  	}
   175  	defaultBlessings, _ := principal.BlessingStore().Default()
   176  	if dumpFlag || defaultBlessings.Expiry().After(time.Now().Add(doNotRefreshDurationFlag)) {
   177  		dump(principal)
   178  		if err = maybeBlessRemotes(ctx); err != nil {
   179  			return err
   180  		}
   181  		return nil
   182  	}
   183  
   184  	var blessings security.Blessings
   185  	if ec2Flag {
   186  		blessings, err = fetchEC2Blessings(ctx)
   187  	} else if k8sFlag {
   188  		blessings, err = fetchK8sBlessings(ctx)
   189  	} else {
   190  		blessings, err = fetchGoogleBlessings(ctx)
   191  	}
   192  	if err != nil {
   193  		return errors.E("failed to fetch blessings", err)
   194  	}
   195  	if expiryCaveatFlag != "" {
   196  		d, err := time.ParseDuration(expiryCaveatFlag)
   197  		if err != nil {
   198  			return errors.E("failed to parse expiry-caveat")
   199  		}
   200  		expiryCaveat, err := security.NewExpiryCaveat(time.Now().Add(d))
   201  		if err != nil {
   202  			return errors.E("failed to make expiry caveat", err)
   203  		}
   204  		extension := fmt.Sprintf("expires-%v", d)
   205  		blessings, err = principal.Bless(principal.PublicKey(), blessings, extension, expiryCaveat)
   206  		if err != nil {
   207  			return errors.E("failed to make expired blessings", err)
   208  		}
   209  	}
   210  	if err = principal.BlessingStore().SetDefault(blessings); err != nil {
   211  		return errors.E(err, "failed to set default blessings")
   212  	}
   213  	_, err = principal.BlessingStore().Set(blessings, security.AllPrincipals)
   214  	if err != nil {
   215  		return errors.E(err, "failed to set peer blessings")
   216  	}
   217  	if err := security.AddToRoots(principal, blessings); err != nil {
   218  		return errors.E(err, "failed to add blessing roots")
   219  	}
   220  
   221  	fmt.Println("Successfully applied new blessing:")
   222  	dump(principal)
   223  	if err = maybeBlessRemotes(ctx); err != nil {
   224  		return err
   225  	}
   226  	return nil
   227  }
   228  
   229  func dump(principal security.Principal) {
   230  	// Mimic the output of the v.io/x/ref/cmd/principal dump command.
   231  	fmt.Printf("Public key: %s\n", principal.PublicKey())
   232  	fmt.Println("---------------- BlessingStore ----------------")
   233  	fmt.Print(principal.BlessingStore().DebugString())
   234  	fmt.Println("---------------- BlessingRoots ----------------")
   235  	fmt.Print(principal.Roots().DebugString())
   236  
   237  	blessing, _ := principal.BlessingStore().Default()
   238  	fmt.Printf("Expires on %s (in %s)\n", blessing.Expiry().Local(), time.Until(blessing.Expiry()))
   239  }
   240  
   241  func maybeBlessRemotes(ctx *context.T) error {
   242  	if !blessRemotesFlag {
   243  		return nil
   244  	}
   245  	// The only use case for blessing remotes is for Google blessings, i.e.
   246  	// using the local browser for OAuth to bless a headless EC2 instance.
   247  	const prefix = "v23.grail.com:google:"
   248  	blessings, _ := v23.GetPrincipal(ctx).BlessingStore().Default()
   249  	if !strings.HasPrefix(blessings.String(), prefix) {
   250  		return nil
   251  	}
   252  	if err := remote.Bless(ctx, blessRemotesTargetsFlag); err != nil {
   253  		return errors.E("failed to send blessings to instances", err)
   254  	}
   255  	return nil
   256  }