github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/apiserver/keymanager/keymanager.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package keymanager 5 6 import ( 7 "fmt" 8 "strings" 9 10 "github.com/juju/errors" 11 "github.com/juju/loggo" 12 "github.com/juju/utils" 13 "github.com/juju/utils/set" 14 "github.com/juju/utils/ssh" 15 "gopkg.in/juju/names.v2" 16 17 "github.com/juju/juju/apiserver/common" 18 "github.com/juju/juju/apiserver/facade" 19 "github.com/juju/juju/apiserver/params" 20 "github.com/juju/juju/environs/config" 21 "github.com/juju/juju/state" 22 ) 23 24 var logger = loggo.GetLogger("juju.apiserver.keymanager") 25 26 func init() { 27 common.RegisterStandardFacade("KeyManager", 1, NewKeyManagerAPI) 28 } 29 30 // The comment values used by juju internal ssh keys. 31 var internalComments = set.NewStrings([]string{"juju-client-key", "juju-system-key"}...) 32 33 // KeyManager defines the methods on the keymanager API end point. 34 type KeyManager interface { 35 ListKeys(arg params.ListSSHKeys) (params.StringsResults, error) 36 AddKeys(arg params.ModifyUserSSHKeys) (params.ErrorResults, error) 37 DeleteKeys(arg params.ModifyUserSSHKeys) (params.ErrorResults, error) 38 ImportKeys(arg params.ModifyUserSSHKeys) (params.ErrorResults, error) 39 } 40 41 // KeyManagerAPI implements the KeyUpdater interface and is the concrete 42 // implementation of the api end point. 43 type KeyManagerAPI struct { 44 state *state.State 45 resources facade.Resources 46 authorizer facade.Authorizer 47 canRead func(string) bool 48 canWrite func(string) bool 49 check *common.BlockChecker 50 } 51 52 var _ KeyManager = (*KeyManagerAPI)(nil) 53 54 // NewKeyManagerAPI creates a new server-side keyupdater API end point. 55 func NewKeyManagerAPI(st *state.State, resources facade.Resources, authorizer facade.Authorizer) (*KeyManagerAPI, error) { 56 // Only clients and environment managers can access the key manager service. 57 if !authorizer.AuthClient() && !authorizer.AuthModelManager() { 58 return nil, common.ErrPerm 59 } 60 env, err := st.Model() 61 if err != nil { 62 return nil, errors.Trace(err) 63 } 64 // For gccgo interface comparisons, we need a Tag. 65 owner := names.Tag(env.Owner()) 66 // TODO(wallyworld) - replace stub with real canRead function 67 // For now, only admins can read authorised ssh keys. 68 canRead := func(user string) bool { 69 // Are we a machine agent operating as the system identity? 70 if user == config.JujuSystemKey { 71 _, ismachinetag := authorizer.GetAuthTag().(names.MachineTag) 72 return ismachinetag 73 } 74 return authorizer.GetAuthTag() == owner 75 } 76 // TODO(wallyworld) - replace stub with real canWrite function 77 // For now, only admins can write authorised ssh keys for users. 78 // Machine agents can write the juju-system-key. 79 canWrite := func(user string) bool { 80 // Are we a machine agent writing the Juju system key. 81 if user == config.JujuSystemKey { 82 _, ismachinetag := authorizer.GetAuthTag().(names.MachineTag) 83 return ismachinetag 84 } 85 // No point looking to see if the user exists as we are not 86 // yet storing keys on the user. 87 return authorizer.GetAuthTag() == owner 88 } 89 return &KeyManagerAPI{ 90 state: st, 91 resources: resources, 92 authorizer: authorizer, 93 canRead: canRead, 94 canWrite: canWrite, 95 check: common.NewBlockChecker(st), 96 }, nil 97 } 98 99 // ListKeys returns the authorised ssh keys for the specified users. 100 func (api *KeyManagerAPI) ListKeys(arg params.ListSSHKeys) (params.StringsResults, error) { 101 if len(arg.Entities.Entities) == 0 { 102 return params.StringsResults{}, nil 103 } 104 results := make([]params.StringsResult, len(arg.Entities.Entities)) 105 106 // For now, authorised keys are global, common to all users. 107 var keyInfo []string 108 cfg, configErr := api.state.ModelConfig() 109 if configErr == nil { 110 keys := ssh.SplitAuthorisedKeys(cfg.AuthorizedKeys()) 111 keyInfo = parseKeys(keys, arg.Mode) 112 } 113 114 for i, entity := range arg.Entities.Entities { 115 // NOTE: entity.Tag isn't a tag, but a username. 116 if !api.canRead(entity.Tag) { 117 results[i].Error = common.ServerError(common.ErrPerm) 118 continue 119 } 120 // All keys are global, no need to look up the user. 121 if configErr == nil { 122 results[i].Result = keyInfo 123 } 124 results[i].Error = common.ServerError(configErr) 125 } 126 return params.StringsResults{Results: results}, nil 127 } 128 129 func parseKeys(keys []string, mode ssh.ListMode) (keyInfo []string) { 130 for _, key := range keys { 131 fingerprint, comment, err := ssh.KeyFingerprint(key) 132 if err != nil { 133 keyInfo = append(keyInfo, fmt.Sprintf("Invalid key: %v", key)) 134 continue 135 } 136 // Only including user added keys not internal ones. 137 if internalComments.Contains(comment) { 138 continue 139 } 140 if mode == ssh.FullKeys { 141 keyInfo = append(keyInfo, key) 142 } else { 143 shortKey := fingerprint 144 if comment != "" { 145 shortKey += fmt.Sprintf(" (%s)", comment) 146 } 147 keyInfo = append(keyInfo, shortKey) 148 } 149 } 150 return keyInfo 151 } 152 153 func (api *KeyManagerAPI) writeSSHKeys(sshKeys []string) error { 154 // Write out the new keys. 155 keyStr := strings.Join(sshKeys, "\n") 156 attrs := map[string]interface{}{config.AuthorizedKeysKey: keyStr} 157 // TODO(waigani) 2014-03-17 bug #1293324 158 // Pass in validation to ensure SSH keys 159 // have not changed underfoot 160 err := api.state.UpdateModelConfig(attrs, nil, nil) 161 if err != nil { 162 return fmt.Errorf("writing environ config: %v", err) 163 } 164 return nil 165 } 166 167 // currentKeyDataForAdd gathers data used when adding ssh keys. 168 func (api *KeyManagerAPI) currentKeyDataForAdd() (keys []string, fingerprints set.Strings, err error) { 169 fingerprints = make(set.Strings) 170 cfg, err := api.state.ModelConfig() 171 if err != nil { 172 return nil, nil, fmt.Errorf("reading current key data: %v", err) 173 } 174 keys = ssh.SplitAuthorisedKeys(cfg.AuthorizedKeys()) 175 for _, key := range keys { 176 fingerprint, _, err := ssh.KeyFingerprint(key) 177 if err != nil { 178 logger.Warningf("ignoring invalid ssh key %q: %v", key, err) 179 } 180 fingerprints.Add(fingerprint) 181 } 182 return keys, fingerprints, nil 183 } 184 185 // AddKeys adds new authorised ssh keys for the specified user. 186 func (api *KeyManagerAPI) AddKeys(arg params.ModifyUserSSHKeys) (params.ErrorResults, error) { 187 if err := api.check.ChangeAllowed(); err != nil { 188 return params.ErrorResults{}, errors.Trace(err) 189 } 190 result := params.ErrorResults{ 191 Results: make([]params.ErrorResult, len(arg.Keys)), 192 } 193 if len(arg.Keys) == 0 { 194 return result, nil 195 } 196 197 if !api.canWrite(arg.User) { 198 return params.ErrorResults{}, common.ServerError(common.ErrPerm) 199 } 200 201 // For now, authorised keys are global, common to all users. 202 sshKeys, currentFingerprints, err := api.currentKeyDataForAdd() 203 if err != nil { 204 return params.ErrorResults{}, common.ServerError(fmt.Errorf("reading current key data: %v", err)) 205 } 206 207 // Ensure we are not going to add invalid or duplicate keys. 208 result.Results = make([]params.ErrorResult, len(arg.Keys)) 209 for i, key := range arg.Keys { 210 fingerprint, _, err := ssh.KeyFingerprint(key) 211 if err != nil { 212 result.Results[i].Error = common.ServerError(fmt.Errorf("invalid ssh key: %s", key)) 213 continue 214 } 215 if currentFingerprints.Contains(fingerprint) { 216 result.Results[i].Error = common.ServerError(fmt.Errorf("duplicate ssh key: %s", key)) 217 continue 218 } 219 sshKeys = append(sshKeys, key) 220 } 221 err = api.writeSSHKeys(sshKeys) 222 if err != nil { 223 return params.ErrorResults{}, common.ServerError(err) 224 } 225 return result, nil 226 } 227 228 type importedSSHKey struct { 229 key string 230 fingerprint string 231 err error 232 } 233 234 // Override for testing 235 var RunSSHImportId = runSSHImportId 236 237 func runSSHImportId(keyId string) (string, error) { 238 return utils.RunCommand("ssh-import-id", "-o", "-", keyId) 239 } 240 241 // runSSHKeyImport uses ssh-import-id to find the ssh keys for the specified key ids. 242 func runSSHKeyImport(keyIds []string) map[string][]importedSSHKey { 243 importResults := make(map[string][]importedSSHKey, len(keyIds)) 244 for _, keyId := range keyIds { 245 keyInfo := []importedSSHKey{} 246 output, err := RunSSHImportId(keyId) 247 if err != nil { 248 keyInfo = append(keyInfo, importedSSHKey{err: err}) 249 continue 250 } 251 lines := strings.Split(output, "\n") 252 hasKey := false 253 for _, line := range lines { 254 if !strings.HasPrefix(line, "ssh-") { 255 continue 256 } 257 hasKey = true 258 // ignore key comment (e.g., user@host) 259 fingerprint, _, err := ssh.KeyFingerprint(line) 260 keyInfo = append(keyInfo, importedSSHKey{ 261 key: line, 262 fingerprint: fingerprint, 263 err: errors.Annotatef(err, "invalid ssh key for %s", keyId), 264 }) 265 } 266 if !hasKey { 267 keyInfo = append(keyInfo, importedSSHKey{ 268 err: errors.Errorf("invalid ssh key id: %s", keyId), 269 }) 270 } 271 importResults[keyId] = keyInfo 272 } 273 return importResults 274 } 275 276 // ImportKeys imports new authorised ssh keys from the specified key ids for the specified user. 277 func (api *KeyManagerAPI) ImportKeys(arg params.ModifyUserSSHKeys) (params.ErrorResults, error) { 278 if err := api.check.ChangeAllowed(); err != nil { 279 return params.ErrorResults{}, errors.Trace(err) 280 } 281 result := params.ErrorResults{ 282 Results: make([]params.ErrorResult, len(arg.Keys)), 283 } 284 if len(arg.Keys) == 0 { 285 return result, nil 286 } 287 288 if !api.canWrite(arg.User) { 289 return params.ErrorResults{}, common.ServerError(common.ErrPerm) 290 } 291 292 // For now, authorised keys are global, common to all users. 293 sshKeys, currentFingerprints, err := api.currentKeyDataForAdd() 294 if err != nil { 295 return params.ErrorResults{}, common.ServerError(fmt.Errorf("reading current key data: %v", err)) 296 } 297 298 importedKeyInfo := runSSHKeyImport(arg.Keys) 299 // Ensure we are not going to add invalid or duplicate keys. 300 result.Results = make([]params.ErrorResult, len(importedKeyInfo)) 301 for i, key := range arg.Keys { 302 compoundErr := "" 303 for _, keyInfo := range importedKeyInfo[key] { 304 if keyInfo.err != nil { 305 compoundErr += fmt.Sprintf("%v\n", keyInfo.err) 306 continue 307 } 308 if currentFingerprints.Contains(keyInfo.fingerprint) { 309 compoundErr += fmt.Sprintf("%v\n", errors.Errorf("duplicate ssh key: %s", keyInfo.key)) 310 continue 311 } 312 sshKeys = append(sshKeys, keyInfo.key) 313 } 314 if compoundErr != "" { 315 result.Results[i].Error = common.ServerError(errors.Errorf(strings.TrimSuffix(compoundErr, "\n"))) 316 } 317 318 } 319 err = api.writeSSHKeys(sshKeys) 320 if err != nil { 321 return params.ErrorResults{}, common.ServerError(err) 322 } 323 return result, nil 324 } 325 326 // currentKeyDataForDelete gathers data used when deleting ssh keys. 327 func (api *KeyManagerAPI) currentKeyDataForDelete() ( 328 currentKeys []string, byFingerprint map[string]string, byComment map[string]string, err error) { 329 330 cfg, err := api.state.ModelConfig() 331 if err != nil { 332 return nil, nil, nil, fmt.Errorf("reading current key data: %v", err) 333 } 334 // For now, authorised keys are global, common to all users. 335 currentKeys = ssh.SplitAuthorisedKeys(cfg.AuthorizedKeys()) 336 337 // Make two maps that index keys by fingerprint and by comment for fast 338 // lookup of keys to delete which may be given as either. 339 byFingerprint = make(map[string]string) 340 byComment = make(map[string]string) 341 for _, key := range currentKeys { 342 fingerprint, comment, err := ssh.KeyFingerprint(key) 343 if err != nil { 344 logger.Debugf("keeping unrecognised existing ssh key %q: %v", key, err) 345 continue 346 } 347 byFingerprint[fingerprint] = key 348 if comment != "" { 349 byComment[comment] = key 350 } 351 } 352 return currentKeys, byFingerprint, byComment, nil 353 } 354 355 // DeleteKeys deletes the authorised ssh keys for the specified user. 356 func (api *KeyManagerAPI) DeleteKeys(arg params.ModifyUserSSHKeys) (params.ErrorResults, error) { 357 if err := api.check.ChangeAllowed(); err != nil { 358 return params.ErrorResults{}, errors.Trace(err) 359 } 360 result := params.ErrorResults{ 361 Results: make([]params.ErrorResult, len(arg.Keys)), 362 } 363 if len(arg.Keys) == 0 { 364 return result, nil 365 } 366 367 if !api.canWrite(arg.User) { 368 return params.ErrorResults{}, common.ServerError(common.ErrPerm) 369 } 370 371 allKeys, byFingerprint, byComment, err := api.currentKeyDataForDelete() 372 if err != nil { 373 return params.ErrorResults{}, common.ServerError(fmt.Errorf("reading current key data: %v", err)) 374 } 375 376 // Record the keys to be deleted in the second pass. 377 keysToDelete := make(set.Strings) 378 379 // Find the keys corresponding to the specified key fingerprints or comments. 380 for i, keyId := range arg.Keys { 381 // Is given keyId a fingerprint? 382 key, ok := byFingerprint[keyId] 383 if ok { 384 keysToDelete.Add(key) 385 continue 386 } 387 // Not a fingerprint, is it a comment? 388 key, ok = byComment[keyId] 389 if ok { 390 if internalComments.Contains(keyId) { 391 result.Results[i].Error = common.ServerError(fmt.Errorf("may not delete internal key: %s", keyId)) 392 continue 393 } 394 keysToDelete.Add(key) 395 continue 396 } 397 result.Results[i].Error = common.ServerError(fmt.Errorf("invalid ssh key: %s", keyId)) 398 } 399 400 var keysToWrite []string 401 402 // Add back only the keys that are not deleted, preserving the order. 403 for _, key := range allKeys { 404 if !keysToDelete.Contains(key) { 405 keysToWrite = append(keysToWrite, key) 406 } 407 } 408 409 if len(keysToWrite) == 0 { 410 return params.ErrorResults{}, common.ServerError(fmt.Errorf("cannot delete all keys")) 411 } 412 413 err = api.writeSSHKeys(keysToWrite) 414 if err != nil { 415 return params.ErrorResults{}, common.ServerError(err) 416 } 417 return result, nil 418 }