diff --git a/auth/jwt/jwt.go b/auth/jwt/jwt.go index b1b9986d..7c8e7cef 100644 --- a/auth/jwt/jwt.go +++ b/auth/jwt/jwt.go @@ -51,17 +51,21 @@ func (j *jwt) Options() auth.Options { func (j *jwt) Generate(id string, opts ...auth.GenerateOption) (*auth.Account, error) { options := auth.NewGenerateOptions(opts...) + if len(options.Issuer) == 0 { + options.Issuer = j.Options().Issuer + } + account := &auth.Account{ ID: id, Type: options.Type, Scopes: options.Scopes, Metadata: options.Metadata, - Issuer: j.Options().Issuer, + Issuer: options.Issuer, } // generate a JWT secret which can be provided to the Token() method // and exchanged for an access token - secret, err := j.jwt.Generate(account) + secret, err := j.jwt.Generate(account, token.WithExpiry(time.Hour*24*365)) if err != nil { return nil, err } diff --git a/auth/options.go b/auth/options.go index e2da1c57..5aa08582 100644 --- a/auth/options.go +++ b/auth/options.go @@ -130,6 +130,8 @@ type GenerateOptions struct { Type string // Secret used to authenticate the account Secret string + // Issuer of the account, e.g. micro + Issuer string } type GenerateOption func(o *GenerateOptions) @@ -169,6 +171,13 @@ func WithScopes(s ...string) GenerateOption { } } +// WithIssuer for the generated account +func WithIssuer(i string) GenerateOption { + return func(o *GenerateOptions) { + o.Issuer = i + } +} + // NewGenerateOptions from a slice of options func NewGenerateOptions(opts ...GenerateOption) GenerateOptions { var options GenerateOptions diff --git a/config/cmd/cmd.go b/config/cmd/cmd.go index 070a5f9e..b4dda0b9 100644 --- a/config/cmd/cmd.go +++ b/config/cmd/cmd.go @@ -667,10 +667,8 @@ func (c *cmd) Before(ctx *cli.Context) error { (*c.opts.Auth).Init(authOpts...) } - // generate the services auth account. - // todo: move this so it only runs for new services - serverID := (*c.opts.Server).Options().Id - if err := authutil.Generate(serverID, c.App().Name, (*c.opts.Auth)); err != nil { + // verify the auth's service account + if err := authutil.Verify(*c.opts.Auth); err != nil { return err } diff --git a/runtime/default.go b/runtime/default.go index d86f9290..e5abb8e1 100644 --- a/runtime/default.go +++ b/runtime/default.go @@ -269,6 +269,18 @@ func (r *runtime) Create(s *Service, opts ...CreateOption) error { options.Args = []string{"run", "."} } + // pass credentials as env vars + if len(options.Credentials) > 0 { + // validate the creds + comps := strings.Split(options.Credentials, ":") + if len(comps) != 2 { + return errors.New("Invalid credentials, expected format 'user:pass'") + } + + options.Env = append(options.Env, "MICRO_AUTH_ID", comps[0]) + options.Env = append(options.Env, "MICRO_AUTH_SECRET", comps[1]) + } + if _, ok := r.namespaces[options.Namespace]; !ok { r.namespaces[options.Namespace] = make(map[string]*service) } diff --git a/runtime/kubernetes/kubernetes.go b/runtime/kubernetes/kubernetes.go index 461b8875..f9272239 100644 --- a/runtime/kubernetes/kubernetes.go +++ b/runtime/kubernetes/kubernetes.go @@ -2,10 +2,14 @@ package kubernetes import ( + "encoding/base64" + "errors" "fmt" + "strings" "sync" "time" + "github.com/micro/go-micro/v2/logger" log "github.com/micro/go-micro/v2/logger" "github.com/micro/go-micro/v2/runtime" "github.com/micro/go-micro/v2/util/kubernetes/client" @@ -420,10 +424,27 @@ func (k *kubernetes) Create(s *runtime.Service, opts ...runtime.CreateOption) er return err } } - // determine the image from the source and options options.Image = k.getImage(s, options) + // create a secret for the credentials if some where provided + if len(options.Credentials) > 0 { + secret, err := k.createCredentials(s, options) + if err != nil { + if logger.V(logger.WarnLevel, logger.DefaultLogger) { + logger.Warnf("Error generating auth credentials for service: %v", err) + } + return err + } + + if logger.V(logger.DebugLevel, logger.DefaultLogger) { + logger.Debugf("Generated auth credentials for service %v", s.Name) + } + + // pass the secret name to the client via the credentials option + options.Credentials = secret + } + // create new service service := newService(s, options) @@ -530,7 +551,6 @@ func (k *kubernetes) Delete(s *runtime.Service, opts ...runtime.DeleteOption) er options := runtime.DeleteOptions{ Namespace: client.DefaultNamespace, } - for _, o := range opts { o(&options) } @@ -544,7 +564,11 @@ func (k *kubernetes) Delete(s *runtime.Service, opts ...runtime.DeleteOption) er Namespace: options.Namespace, }) - return service.Stop(k.client, client.DeleteNamespace(options.Namespace)) + // delete the service credentials + ns := client.DeleteNamespace(options.Namespace) + k.client.Delete(&client.Resource{Name: credentialsName(s), Kind: "secret"}, ns) + + return service.Stop(k.client, ns) } // Start starts the runtime @@ -643,3 +667,36 @@ func (k *kubernetes) getImage(s *runtime.Service, options runtime.CreateOptions) return "" } +func (k *kubernetes) createCredentials(service *runtime.Service, options runtime.CreateOptions) (string, error) { + // validate the creds + comps := strings.Split(options.Credentials, ":") + if len(comps) != 2 { + return "", errors.New("Invalid credentials, expected format 'user:pass'") + } + + // construct the k8s secret object + secret := &client.Secret{ + Type: "Opaque", + Data: map[string]string{ + "id": base64.StdEncoding.EncodeToString([]byte(comps[0])), + "secret": base64.StdEncoding.EncodeToString([]byte(comps[1])), + }, + Metadata: &client.Metadata{ + Name: credentialsName(service), + Namespace: options.Namespace, + }, + } + + // create options specify the namespace + ns := client.CreateNamespace(options.Namespace) + + // crete the secret in kubernetes + name := credentialsName(service) + err := k.client.Create(&client.Resource{Kind: "secret", Name: name, Value: secret}, ns) + return name, err +} + +func credentialsName(service *runtime.Service) string { + name := fmt.Sprintf("%v-%v-credentials", service.Name, service.Version) + return client.SerializeResourceName(name) +} diff --git a/runtime/kubernetes/service.go b/runtime/kubernetes/service.go index 4cb71e37..6decd16e 100644 --- a/runtime/kubernetes/service.go +++ b/runtime/kubernetes/service.go @@ -75,6 +75,27 @@ func newService(s *runtime.Service, c runtime.CreateOptions) *service { env = append(env, client.EnvVar{Name: evarPair[0], Value: evarPair[1]}) } + // if credentials were provided, pass them to the service + if len(c.Credentials) > 0 { + env = append(env, client.EnvVar{ + Name: "MICRO_AUTH_ID", + ValueFrom: &client.EnvVarSource{ + SecretKeyRef: &client.SecretKeySelector{ + Name: c.Credentials, Key: "id", + }, + }, + }) + + env = append(env, client.EnvVar{ + Name: "MICRO_AUTH_SECRET", + ValueFrom: &client.EnvVarSource{ + SecretKeyRef: &client.SecretKeySelector{ + Name: c.Credentials, Key: "secret", + }, + }, + }) + } + // if environment has been supplied update deployment default environment if len(env) > 0 { kdeploy.Spec.Template.PodSpec.Containers[0].Env = append(kdeploy.Spec.Template.PodSpec.Containers[0].Env, env...) diff --git a/runtime/options.go b/runtime/options.go index dea936b9..95540315 100644 --- a/runtime/options.go +++ b/runtime/options.go @@ -82,6 +82,8 @@ type CreateOptions struct { Namespace string // Specify the context to use Context context.Context + // Credentials for the service to use + Credentials string } // ReadOptions queries runtime services @@ -126,6 +128,13 @@ func CreateContext(ctx context.Context) CreateOption { } } +// CreateCredentials sets the credentials to start the service with +func CreateCredentials(user, pass string) CreateOption { + return func(o *CreateOptions) { + o.Credentials = user + ":" + pass + } +} + // WithCommand specifies the command to execute func WithCommand(cmd ...string) CreateOption { return func(o *CreateOptions) { diff --git a/util/auth/auth.go b/util/auth/auth.go index 70a47f8a..9f8e672e 100644 --- a/util/auth/auth.go +++ b/util/auth/auth.go @@ -1,34 +1,31 @@ package auth import ( - "fmt" "time" + "github.com/google/uuid" "github.com/micro/go-micro/v2/auth" "github.com/micro/go-micro/v2/logger" ) -// Generate generates a service account for and continually -// refreshes the access token. -func Generate(id, name string, a auth.Auth) error { +// Verify the auth credentials and refresh the auth token periodicallay +func Verify(a auth.Auth) error { // extract the account creds from options, these can be set by flags accID := a.Options().ID accSecret := a.Options().Secret - // if no credentials were provided, generate an account - if len(accID) == 0 || len(accSecret) == 0 { - name := fmt.Sprintf("%v-%v", name, id) - + // if no credentials were provided, self generate an account + if len(accID) == 0 && len(accSecret) == 0 { opts := []auth.GenerateOption{ auth.WithType("service"), auth.WithScopes("service"), } - acc, err := a.Generate(name, opts...) + acc, err := a.Generate(uuid.New().String(), opts...) if err != nil { return err } - logger.Debugf("Auth [%v] Authenticated as %v issued by %v", a, name, acc.Issuer) + logger.Debugf("Auth [%v] Self-generated an auth account", a.String()) accID = acc.ID accSecret = acc.Secret diff --git a/util/kubernetes/client/types.go b/util/kubernetes/client/types.go index 093fe92b..cfc40235 100644 --- a/util/kubernetes/client/types.go +++ b/util/kubernetes/client/types.go @@ -212,7 +212,7 @@ type ImagePullSecret struct { type Secret struct { Type string `json:"type,omitempty"` Data map[string]string `json:"data"` - Metadata *Metadata `json:"metadata"` + Metadata *Metadata `json:"metadata,omitempty"` } // ServiceAccount diff --git a/web/service.go b/web/service.go index 8c216326..70fe5d5f 100644 --- a/web/service.go +++ b/web/service.go @@ -449,9 +449,7 @@ func (s *service) Init(opts ...Option) error { func (s *service) Run() error { // generate an auth account - srvID := s.opts.Service.Server().Options().Id - srvName := s.Options().Name - if err := authutil.Generate(srvID, srvName, s.opts.Service.Options().Auth); err != nil { + if err := authutil.Verify(s.opts.Service.Options().Auth); err != nil { return err }