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(®ionFlag, "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 }