github.com/grailbio/base@v0.0.11/cmd/ticket-server/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  	"errors"
    12  	"io/ioutil"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/aws/aws-sdk-go/aws"
    17  	"github.com/aws/aws-sdk-go/aws/session"
    18  	"github.com/grailbio/base/cmd/ticket-server/config"
    19  	"github.com/grailbio/base/common/log"
    20  	"github.com/grailbio/base/security/identity"
    21  	_ "github.com/grailbio/base/security/keycrypt/file"
    22  	_ "github.com/grailbio/base/security/keycrypt/keychain"
    23  	_ "github.com/grailbio/base/security/keycrypt/kms"
    24  	"github.com/grailbio/base/security/ticket"
    25  	_ "github.com/grailbio/v23/factories/grail"
    26  	"golang.org/x/oauth2/google"
    27  	"golang.org/x/oauth2/jwt"
    28  	admin "google.golang.org/api/admin/directory/v1"
    29  	v23 "v.io/v23"
    30  	"v.io/v23/context"
    31  	"v.io/v23/glob"
    32  	"v.io/v23/naming"
    33  	"v.io/v23/rpc"
    34  	"v.io/v23/security"
    35  	"v.io/x/lib/cmdline"
    36  	"v.io/x/ref/lib/security/securityflag"
    37  	"v.io/x/ref/lib/signals"
    38  	"v.io/x/ref/lib/v23cmd"
    39  )
    40  
    41  var (
    42  	nameFlag            string
    43  	configDirFlag       string
    44  	regionFlag          string
    45  	googleUserSufixFlag string
    46  	googleAdminNameFlag string
    47  
    48  	dryrunFlag bool
    49  
    50  	googleExpirationIntervalFlag time.Duration
    51  	serviceAccountFlag           string
    52  
    53  	ec2BlesserRoleFlag             string
    54  	ec2ExpirationIntervalFlag      time.Duration
    55  	ec2DynamoDBTableFlag           string
    56  	ec2DisableAddrCheckFlag        bool
    57  	ec2DisableUniquenessCheckFlag  bool
    58  	ec2DisablePendingTimeCheckFlag bool
    59  
    60  	k8sBlesserRoleFlag        string
    61  	k8sExpirationIntervalFlag time.Duration
    62  	awsAccountsFlag           string
    63  	awsRegionsFlag            string
    64  )
    65  
    66  func newCmdRoot() *cmdline.Command {
    67  	root := &cmdline.Command{
    68  		Runner: v23cmd.RunnerFunc(run),
    69  		Name:   "ticket-server",
    70  		Short:  "Runs a Vanadium server that allows restricted access to tickets",
    71  		Long: `
    72  Command ticket-server runs a Vanadium server that provides restricted access to
    73  tickets. A ticket contains credentials and configurations that allows
    74  communicating with another system. For example, an S3 ticket contains AWS
    75  credentials and also the bucket and object or prefix to fetch while a Docker
    76  ticket contains the TLS certificate expected from the server, a client TLS
    77  certificate + the private key and the URL to reach the Docker daemon.
    78  `,
    79  	}
    80  	root.Flags.StringVar(&nameFlag, "name", "", "Name to mount the server under. If empty, don't mount.")
    81  	root.Flags.StringVar(&configDirFlag, "config-dir", "", "Directory with tickets in VDL format. Must be provided.")
    82  	root.Flags.BoolVar(&dryrunFlag, "dry-run", false, "Don't run, just check the configs.")
    83  	root.Flags.StringVar(&regionFlag, "region", "us-west-2", "AWS region to use for cached AWS session.")
    84  	root.Flags.DurationVar(&googleExpirationIntervalFlag, "google-expiration", 7*24*time.Hour, "Expiration caveat for the Google-based blessings.")
    85  	root.Flags.StringVar(&serviceAccountFlag, "service-account", "", "JSON file with a Google service account credentials.")
    86  	root.Flags.StringVar(&ec2BlesserRoleFlag, "ec2-blesser-role", "", "What role to use for the blesser/ec2 endpoint. The role needs to exist in all the accounts.")
    87  	root.Flags.DurationVar(&ec2ExpirationIntervalFlag, "ec2-expiration", 365*24*time.Hour, "Expiration caveat for the EC2-based blessings.")
    88  	root.Flags.StringVar(&ec2DynamoDBTableFlag, "ec2-dynamodb-table", "", "DynamoDB table to use for enforcing the uniqueness of the EC2-based blessings requests.")
    89  	root.Flags.BoolVar(&ec2DisableAddrCheckFlag, "danger-danger-danger-ec2-disable-address-check", false, "Disable the IP address check for the EC2-based blessings requests. Only useful for local tests.")
    90  	root.Flags.BoolVar(&ec2DisableUniquenessCheckFlag, "danger-danger-danger-ec2-disable-uniqueness-check", false, "Disable the uniqueness check for the EC2-based blessings requests. Only useful for local tests.")
    91  	root.Flags.BoolVar(&ec2DisablePendingTimeCheckFlag, "danger-danger-danger-ec2-disable-pending-time-check", false, "Disable the pendint time check for the EC2-based blessings requests. Only useful for local tests.")
    92  
    93  	root.Flags.StringVar(&googleUserSufixFlag, "google-user-domain", "grailbio.com", "Comma-separated list of email domains used for validating users.")
    94  	root.Flags.StringVar(&googleAdminNameFlag, "google-admin", "admin@grailbio.com", "Google Admin that can read all group memberships - NOTE: all groups will need to match the admin user's domain.")
    95  
    96  	root.Flags.DurationVar(&k8sExpirationIntervalFlag, "k8s-expiration", 365*24*time.Hour, "Expiration caveat for the K8s-based blessings.")
    97  	root.Flags.StringVar(&k8sBlesserRoleFlag, "k8s-blesser-role", "ticket-server", "What role to use to lookup EKS cluster information on all authorized accounts. The role needs to exist in all the accounts.")
    98  	root.Flags.StringVar(&awsAccountsFlag, "aws-account-ids", "", "Commma-separated list of AWS account IDs used to populate allow-list of k8s clusters.")
    99  	root.Flags.StringVar(&awsRegionsFlag, "aws-regions", "us-west-2", "Commma-separated list of AWS regions used to populate allow-list of k8s clusters.")
   100  
   101  	return root
   102  }
   103  
   104  // node describes an inner node in the config tree. The leaves are of type
   105  // service.
   106  type node struct {
   107  	name     string
   108  	children map[string]interface{}
   109  }
   110  
   111  var _ rpc.AllGlobber = (*node)(nil)
   112  
   113  func (n *node) Glob__(ctx *context.T, call rpc.GlobServerCall, g *glob.Glob) error { // nolint: golint
   114  	log.Info(ctx, "glob request", "glob", g, "blessing", call.Security().RemoteBlessings(), "ticket", call.Suffix())
   115  
   116  	sender := call.SendStream()
   117  	element := g.Head()
   118  
   119  	// The key is the path to a node.
   120  	children := map[string]interface{}{"": n}
   121  	for g.Len() != 0 {
   122  		children = descent(children)
   123  		matches := map[string]interface{}{}
   124  		for k, v := range children {
   125  			v := v.(*node)
   126  			if element.Match(v.name) {
   127  				matches[k] = v
   128  			}
   129  		}
   130  		children = matches
   131  		g = g.Tail()
   132  		element = g.Head()
   133  	}
   134  
   135  	if g.String() == "..." {
   136  		matches := map[string]interface{}{}
   137  		for k1, v1 := range children {
   138  			v1 := v1.(*node)
   139  			for k2, v2 := range v1.flatten(k1) {
   140  				matches[k2] = v2
   141  			}
   142  		}
   143  		children = matches
   144  	}
   145  
   146  	for k, v := range children {
   147  		isLeaf := false
   148  		switch v.(type) {
   149  		case *node:
   150  			isLeaf = len(v.(*node).children) == 0
   151  		case *entry:
   152  			isLeaf = true
   153  		}
   154  		sender.Send(naming.GlobReplyEntry{
   155  			Value: naming.MountEntry{
   156  				Name:   strings.TrimLeft(k, "/"),
   157  				IsLeaf: isLeaf,
   158  			},
   159  		})
   160  	}
   161  
   162  	return nil
   163  }
   164  
   165  // flatten expands a node recursively. A node "a" with two empty children "b"
   166  // and "c" should return a map with keys: "a", "a/b", "a/c". The values are
   167  // pointers to node structs.
   168  func (n *node) flatten(prefix string) map[string]interface{} {
   169  	r := map[string]interface{}{}
   170  	for _, v1 := range n.children {
   171  		v1 := v1.(*node)
   172  		k1 := naming.Join(prefix, v1.name)
   173  		r[k1] = v1
   174  		for k2, v2 := range v1.flatten(k1) {
   175  			v2 := v2.(*node)
   176  			r[k2] = v2
   177  		}
   178  	}
   179  	return r
   180  }
   181  
   182  func descent(m map[string]interface{}) map[string]interface{} {
   183  	r := map[string]interface{}{}
   184  	for k1, v1 := range m {
   185  		v1 := v1.(*node)
   186  		for k2, v2 := range v1.children {
   187  			r[k1+"/"+k2] = v2
   188  		}
   189  	}
   190  	return r
   191  }
   192  
   193  type entry struct {
   194  	kind    string
   195  	service interface{}
   196  	auth    security.Authorizer
   197  }
   198  
   199  type dispatcher struct {
   200  	registry map[string]entry
   201  	root     *node
   202  }
   203  
   204  var d *dispatcher
   205  
   206  func newDispatcher(ctx *context.T, awsSession *session.Session, cfg config.Config, jwtConfig *jwt.Config) rpc.Dispatcher {
   207  	d = &dispatcher{
   208  		registry: make(map[string]entry),
   209  		root:     &node{},
   210  	}
   211  
   212  	// Note that the blesser/ endpoints are not exposed via Glob__ and the
   213  	// permissions are governed by the -v23.permissions.{file,literal} flags.
   214  	d.registry["blesser/google"] = entry{
   215  		service: identity.GoogleBlesserServer(newGoogleBlesser(ctx, googleExpirationIntervalFlag,
   216  			strings.Split(googleUserSufixFlag, ","))),
   217  		auth: securityflag.NewAuthorizerOrDie(ctx),
   218  	}
   219  	d.registry["blesser/k8s"] = entry{
   220  		service: identity.K8sBlesserServer(newK8sBlesser(newSessionWrapper(awsSession), k8sExpirationIntervalFlag, k8sBlesserRoleFlag, strings.Split(awsAccountsFlag, ","), strings.Split(awsRegionsFlag, ","))),
   221  		auth:    securityflag.NewAuthorizerOrDie(ctx),
   222  	}
   223  	if ec2BlesserRoleFlag != "" {
   224  		d.registry["blesser/ec2"] = entry{
   225  			service: identity.Ec2BlesserServer(newEc2Blesser(ctx, awsSession, ec2ExpirationIntervalFlag, ec2BlesserRoleFlag, ec2DynamoDBTableFlag)),
   226  			auth:    securityflag.NewAuthorizerOrDie(ctx),
   227  		}
   228  	}
   229  	d.registry["list"] = entry{
   230  		service: ticket.ListServiceServer(newList(ctx)),
   231  		auth:    securityflag.NewAuthorizerOrDie(ctx),
   232  	}
   233  
   234  	for k, v := range cfg {
   235  		auth := googleGroupsAuthorizer(ctx, v.Perms, jwtConfig, googleAdminNameFlag)
   236  		log.Debug(ctx, "adding service to dispatcher registry", "name", k, "perms", v.Perms)
   237  		parts := strings.Split(k, "/")
   238  		n := d.root
   239  		for _, p := range parts {
   240  			if n.children == nil {
   241  				n.children = map[string]interface{}{}
   242  			}
   243  			if next, ok := n.children[p]; ok {
   244  				n = next.(*node)
   245  			} else {
   246  				n.children[p] = &node{name: p}
   247  				n = n.children[p].(*node)
   248  			}
   249  		}
   250  
   251  		d.registry[k] = entry{
   252  			service: ticket.TicketServiceServer(&service{
   253  				name:       parts[len(parts)-1],
   254  				kind:       v.Kind,
   255  				ticket:     v.Ticket,
   256  				perms:      v.Perms,
   257  				awsSession: awsSession,
   258  				controls:   v.Controls,
   259  			}),
   260  			auth: auth,
   261  		}
   262  	}
   263  	return d
   264  }
   265  
   266  // Lookup implements the Dispatcher interface from v.io/v23/rpc.
   267  func (d *dispatcher) Lookup(ctx *context.T, suffix string) (interface{}, security.Authorizer, error) {
   268  	log.Debug(ctx, "performing service looking", "name", suffix)
   269  	if s, ok := d.registry[suffix]; ok {
   270  		return s.service, s.auth, nil
   271  	}
   272  	return d.root, security.DefaultAuthorizer(), nil
   273  }
   274  
   275  func run(ctx *context.T, env *cmdline.Env, args []string) error {
   276  	if configDirFlag == "" {
   277  		return errors.New("-config-dir flag is required")
   278  	}
   279  
   280  	ticketConfig, err := config.Load(configDirFlag)
   281  	if err != nil {
   282  		return err
   283  	}
   284  
   285  	if dryrunFlag {
   286  		return nil
   287  	}
   288  
   289  	if serviceAccountFlag == "" {
   290  		return errors.New("-service-account flag is required")
   291  	}
   292  
   293  	blessings, _ := v23.GetPrincipal(ctx).BlessingStore().Default()
   294  	log.Debug(ctx, "using default blessing", "blessing", blessings)
   295  
   296  	awsSession, err := session.NewSession(aws.NewConfig().WithRegion(regionFlag))
   297  	if err != nil {
   298  		return err
   299  	}
   300  
   301  	serviceAccountJSON, err := ioutil.ReadFile(serviceAccountFlag)
   302  	if err != nil {
   303  		return err
   304  	}
   305  	jwtConfig, err := google.JWTConfigFromJSON(serviceAccountJSON, admin.AdminDirectoryGroupMemberReadonlyScope+" "+admin.AdminDirectoryGroupReadonlyScope)
   306  	if err != nil {
   307  		return err
   308  	}
   309  
   310  	dispatcher := newDispatcher(ctx, awsSession, ticketConfig, jwtConfig)
   311  	_, s, err := v23.WithNewDispatchingServer(ctx, nameFlag, dispatcher)
   312  	if err != nil {
   313  		return err
   314  	}
   315  
   316  	for _, endpoint := range s.Status().Endpoints {
   317  		log.Info(ctx, "server endpoint", "addr", endpoint.Name())
   318  	}
   319  	<-signals.ShutdownOnSignals(ctx) // Wait forever.
   320  	return nil
   321  }
   322  
   323  func main() {
   324  	cmdline.HideGlobalFlagsExcept()
   325  	cmdline.Main(newCmdRoot())
   326  }