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 }