github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/cloud/credentials.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package cloud
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/juju/errors"
    13  	"github.com/juju/schema"
    14  	"github.com/juju/utils"
    15  	"gopkg.in/juju/environschema.v1"
    16  	"gopkg.in/yaml.v2"
    17  )
    18  
    19  // CloudCredential contains attributes used to define credentials for a cloud.
    20  type CloudCredential struct {
    21  	// DefaultCredential is the named credential to use by default.
    22  	DefaultCredential string `yaml:"default-credential,omitempty"`
    23  
    24  	// DefaultRegion is the cloud region to use by default.
    25  	DefaultRegion string `yaml:"default-region,omitempty"`
    26  
    27  	// AuthCredentials is the credentials for a cloud, keyed on name.
    28  	AuthCredentials map[string]Credential `yaml:",omitempty,inline"`
    29  }
    30  
    31  // Credential instances represent cloud credentials.
    32  type Credential struct {
    33  	authType   AuthType
    34  	attributes map[string]string
    35  
    36  	// Label is optionally set to describe the credentials to a user.
    37  	Label string
    38  }
    39  
    40  // AuthType returns the authentication type.
    41  func (c Credential) AuthType() AuthType {
    42  	return c.authType
    43  }
    44  
    45  func copyStringMap(in map[string]string) map[string]string {
    46  	out := make(map[string]string)
    47  	for k, v := range in {
    48  		out[k] = v
    49  	}
    50  	return out
    51  }
    52  
    53  // Attributes returns the credential attributes.
    54  func (c Credential) Attributes() map[string]string {
    55  	return copyStringMap(c.attributes)
    56  }
    57  
    58  // MarshalYAML implements the yaml.Marshaler interface.
    59  func (c Credential) MarshalYAML() (interface{}, error) {
    60  	return struct {
    61  		AuthType   AuthType          `yaml:"auth-type"`
    62  		Attributes map[string]string `yaml:",omitempty,inline"`
    63  	}{c.authType, c.attributes}, nil
    64  }
    65  
    66  // NewCredential returns a new, immutable, Credential with the supplied
    67  // auth-type and attributes.
    68  func NewCredential(authType AuthType, attributes map[string]string) Credential {
    69  	return Credential{authType: authType, attributes: copyStringMap(attributes)}
    70  }
    71  
    72  // NewEmptyCredential returns a new Credential with the EmptyAuthType
    73  // auth-type.
    74  func NewEmptyCredential() Credential {
    75  	return Credential{authType: EmptyAuthType, attributes: nil}
    76  }
    77  
    78  // NewEmptyCloudCredential returns a new CloudCredential with an empty
    79  // default credential.
    80  func NewEmptyCloudCredential() *CloudCredential {
    81  	return &CloudCredential{AuthCredentials: map[string]Credential{"default": NewEmptyCredential()}}
    82  }
    83  
    84  // NamedCredentialAttr describes the properties of a named credential attribute.
    85  type NamedCredentialAttr struct {
    86  	// Name is the name of the credential value.
    87  	Name string
    88  
    89  	// CredentialAttr holds the properties of the credential value.
    90  	CredentialAttr
    91  }
    92  
    93  // CredentialSchema describes the schema of a credential. Credential schemas
    94  // are specific to cloud providers.
    95  type CredentialSchema []NamedCredentialAttr
    96  
    97  // Attribute returns the named CredentialAttr value.
    98  func (s CredentialSchema) Attribute(name string) (*CredentialAttr, bool) {
    99  	for _, value := range s {
   100  		if value.Name == name {
   101  			result := value.CredentialAttr
   102  			return &result, true
   103  		}
   104  	}
   105  	return nil, false
   106  }
   107  
   108  // FinalizeCredential finalizes a credential by matching it with one of the
   109  // provided credential schemas, and reading any file attributes into their
   110  // corresponding non-file attributes. This will also validate the credential.
   111  //
   112  // If there is no schema with the matching auth-type, and error satisfying
   113  // errors.IsNotSupported will be returned.
   114  func FinalizeCredential(
   115  	credential Credential,
   116  	schemas map[AuthType]CredentialSchema,
   117  	readFile func(string) ([]byte, error),
   118  ) (*Credential, error) {
   119  	schema, ok := schemas[credential.authType]
   120  	if !ok {
   121  		return nil, errors.NotSupportedf("auth-type %q", credential.authType)
   122  	}
   123  	attrs, err := schema.Finalize(credential.attributes, readFile)
   124  	if err != nil {
   125  		return nil, errors.Trace(err)
   126  	}
   127  	return &Credential{authType: credential.authType, attributes: attrs}, nil
   128  }
   129  
   130  // Finalize finalizes the given credential attributes against the credential
   131  // schema. If the attributes are invalid, Finalize will return an error.
   132  //
   133  // An updated attribute map will be returned, having any file attributes
   134  // deleted, and replaced by their non-file counterparts with the values set
   135  // to the contents of the files.
   136  func (s CredentialSchema) Finalize(
   137  	attrs map[string]string,
   138  	readFile func(string) ([]byte, error),
   139  ) (map[string]string, error) {
   140  	checker, err := s.schemaChecker()
   141  	if err != nil {
   142  		return nil, errors.Trace(err)
   143  	}
   144  	m := make(map[string]interface{})
   145  	for k, v := range attrs {
   146  		m[k] = v
   147  	}
   148  	result, err := checker.Coerce(m, nil)
   149  	if err != nil {
   150  		return nil, errors.Trace(err)
   151  	}
   152  
   153  	resultMap := result.(map[string]interface{})
   154  	newAttrs := make(map[string]string)
   155  
   156  	// Construct the final credential attributes map, reading values from files as necessary.
   157  	for _, field := range s {
   158  		if field.FileAttr != "" {
   159  			if err := s.processFileAttrValue(field, resultMap, newAttrs, readFile); err != nil {
   160  				return nil, errors.Trace(err)
   161  			}
   162  			continue
   163  		}
   164  		name := field.Name
   165  		if field.FilePath {
   166  			pathValue, ok := resultMap[name]
   167  			if ok && pathValue != "" {
   168  				if absPath, err := ValidateFileAttrValue(pathValue.(string)); err != nil {
   169  					return nil, errors.Trace(err)
   170  				} else {
   171  					newAttrs[name] = absPath
   172  					continue
   173  				}
   174  			}
   175  		}
   176  		if val, ok := resultMap[name]; ok {
   177  			newAttrs[name] = val.(string)
   178  		}
   179  	}
   180  	return newAttrs, nil
   181  }
   182  
   183  // ValidateFileAttrValue returns the normalised file path, so
   184  // long as the specified path is valid and not a directory.
   185  func ValidateFileAttrValue(path string) (string, error) {
   186  	if !filepath.IsAbs(path) && !strings.HasPrefix(path, "~") {
   187  		return "", errors.Errorf("file path must be an absolute path: %s", path)
   188  	}
   189  	absPath, err := utils.NormalizePath(path)
   190  	if err != nil {
   191  		return "", err
   192  	}
   193  	info, err := os.Stat(absPath)
   194  	if err != nil {
   195  		return "", errors.Errorf("invalid file path: %s", absPath)
   196  	}
   197  	if info.IsDir() {
   198  		return "", errors.Errorf("file path must be a file: %s", absPath)
   199  	}
   200  	return absPath, nil
   201  }
   202  
   203  func (s CredentialSchema) processFileAttrValue(
   204  	field NamedCredentialAttr, resultMap map[string]interface{}, newAttrs map[string]string,
   205  	readFile func(string) ([]byte, error),
   206  ) error {
   207  	name := field.Name
   208  	if fieldVal, ok := resultMap[name]; ok {
   209  		if _, ok := resultMap[field.FileAttr]; ok {
   210  			return errors.NotValidf(
   211  				"specifying both %q and %q",
   212  				name, field.FileAttr,
   213  			)
   214  		}
   215  		newAttrs[name] = fieldVal.(string)
   216  		return nil
   217  	}
   218  	fieldVal, ok := resultMap[field.FileAttr]
   219  	if !ok {
   220  		return errors.NewNotValid(nil, fmt.Sprintf(
   221  			"either %q or %q must be specified",
   222  			name, field.FileAttr,
   223  		))
   224  	}
   225  	data, err := readFile(fieldVal.(string))
   226  	if err != nil {
   227  		return errors.Annotatef(err, "reading file for %q", name)
   228  	}
   229  	if len(data) == 0 {
   230  		return errors.NotValidf("empty file for %q", name)
   231  	}
   232  	newAttrs[name] = string(data)
   233  	return nil
   234  }
   235  
   236  func (s CredentialSchema) schemaChecker() (schema.Checker, error) {
   237  	fields := make(environschema.Fields)
   238  	for _, field := range s {
   239  		fields[field.Name] = environschema.Attr{
   240  			Description: field.Description,
   241  			Type:        environschema.Tstring,
   242  			Group:       environschema.AccountGroup,
   243  			Mandatory:   field.FileAttr == "" && !field.Optional,
   244  			Secret:      field.Hidden,
   245  			Values:      field.Options,
   246  		}
   247  	}
   248  	// TODO(axw) add support to environschema for attributes whose values
   249  	// can be read in from a file.
   250  	for _, field := range s {
   251  		if field.FileAttr == "" {
   252  			continue
   253  		}
   254  		if _, ok := fields[field.FileAttr]; ok {
   255  			return nil, errors.Errorf("duplicate field %q", field.FileAttr)
   256  		}
   257  		fields[field.FileAttr] = environschema.Attr{
   258  			Description: field.Description + " (file)",
   259  			Type:        environschema.Tstring,
   260  			Group:       environschema.AccountGroup,
   261  			Mandatory:   false,
   262  			Secret:      false,
   263  		}
   264  	}
   265  
   266  	schemaFields, schemaDefaults, err := fields.ValidationSchema()
   267  	if err != nil {
   268  		return nil, errors.Trace(err)
   269  	}
   270  	return schema.StrictFieldMap(schemaFields, schemaDefaults), nil
   271  }
   272  
   273  // CredentialAttr describes the properties of a credential attribute.
   274  type CredentialAttr struct {
   275  	// Description is a human-readable description of the credential
   276  	// attribute.
   277  	Description string
   278  
   279  	// Hidden controls whether or not the attribute value will be hidden
   280  	// when being entered interactively. Regardless of this, all credential
   281  	// attributes are provided only to the Juju controllers.
   282  	Hidden bool
   283  
   284  	// FileAttr is the name of an attribute that may be specified instead
   285  	// of this one, which points to a file that will be read in and its
   286  	// value used for this attribute.
   287  	FileAttr string
   288  
   289  	// FilePath is true is the value of this attribute is a file path.
   290  	FilePath bool
   291  
   292  	// Optional controls whether the attribute is required to have a non-empty
   293  	// value or not. Attributes default to mandatory.
   294  	Optional bool
   295  
   296  	// Options, if set, define the allowed values for this field.
   297  	Options []interface{}
   298  }
   299  
   300  type cloudCredentialChecker struct{}
   301  
   302  func (c cloudCredentialChecker) Coerce(v interface{}, path []string) (interface{}, error) {
   303  	out := CloudCredential{
   304  		AuthCredentials: make(map[string]Credential),
   305  	}
   306  	v, err := schema.StringMap(cloudCredentialValueChecker{}).Coerce(v, path)
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  	mapv := v.(map[string]interface{})
   311  	for k, v := range mapv {
   312  		switch k {
   313  		case "default-region":
   314  			out.DefaultRegion = v.(string)
   315  		case "default-credential":
   316  			out.DefaultCredential = v.(string)
   317  		default:
   318  			out.AuthCredentials[k] = v.(Credential)
   319  		}
   320  	}
   321  	return out, nil
   322  }
   323  
   324  type cloudCredentialValueChecker struct{}
   325  
   326  func (c cloudCredentialValueChecker) Coerce(v interface{}, path []string) (interface{}, error) {
   327  	field := path[len(path)-1]
   328  	switch field {
   329  	case "default-region", "default-credential":
   330  		return schema.String().Coerce(v, path)
   331  	}
   332  	v, err := schema.StringMap(schema.String()).Coerce(v, path)
   333  	if err != nil {
   334  		return nil, err
   335  	}
   336  	mapv := v.(map[string]interface{})
   337  
   338  	authType, _ := mapv["auth-type"].(string)
   339  	if authType == "" {
   340  		return nil, errors.Errorf("%v: missing auth-type", strings.Join(path, ""))
   341  	}
   342  
   343  	attrs := make(map[string]string)
   344  	delete(mapv, "auth-type")
   345  	for k, v := range mapv {
   346  		attrs[k] = v.(string)
   347  	}
   348  	return Credential{authType: AuthType(authType), attributes: attrs}, nil
   349  }
   350  
   351  // ParseCredentials parses the given yaml bytes into Credentials, but does
   352  // not validate the credential attributes.
   353  func ParseCredentials(data []byte) (map[string]CloudCredential, error) {
   354  	var credentialsYAML struct {
   355  		Credentials map[string]interface{} `yaml:"credentials"`
   356  	}
   357  	err := yaml.Unmarshal(data, &credentialsYAML)
   358  	if err != nil {
   359  		return nil, errors.Annotate(err, "cannot unmarshal yaml credentials")
   360  	}
   361  	credentials := make(map[string]CloudCredential)
   362  	for cloud, v := range credentialsYAML.Credentials {
   363  		v, err := cloudCredentialChecker{}.Coerce(
   364  			v, []string{"credentials." + cloud},
   365  		)
   366  		if err != nil {
   367  			return nil, errors.Trace(err)
   368  		}
   369  		credentials[cloud] = v.(CloudCredential)
   370  	}
   371  	return credentials, nil
   372  }
   373  
   374  // RemoveSecrets returns a copy of the given credential with secret fields removed.
   375  func RemoveSecrets(
   376  	credential Credential,
   377  	schemas map[AuthType]CredentialSchema,
   378  ) (*Credential, error) {
   379  	schema, ok := schemas[credential.authType]
   380  	if !ok {
   381  		return nil, errors.NotSupportedf("auth-type %q", credential.authType)
   382  	}
   383  	redactedAttrs := credential.Attributes()
   384  	for _, attr := range schema {
   385  		if attr.Hidden {
   386  			delete(redactedAttrs, attr.Name)
   387  		}
   388  	}
   389  	return &Credential{authType: credential.authType, attributes: redactedAttrs}, nil
   390  }