runtime: provide credentials to services (#1817)
* runtime: inject credentials into service * util/auth: self generate accounts (needed for jwt) * runtime/kubernetes: add logging for creds * runtime/kubernetes: serialize secret name * runtime/kubernetes: remove unused code * runtime/kubernetes: base64 encode secret * runtime/kubernetes: remove metadata from secret * util/kubernetes/client: omit empty secret metadata * util/kubernetes/client: fix secret template * util/kubernetes/client: fix secrets * web: update auth util * util/auth: fix missing arg * extend token expiry * extend token expiry
This commit is contained in:
parent
3480e0a64e
commit
09ec20fded
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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...)
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user