github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/client/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/collections/set" 11 "github.com/juju/collections/transform" 12 "github.com/juju/errors" 13 "github.com/juju/loggo" 14 "github.com/juju/names/v5" 15 "github.com/juju/utils/v3" 16 "github.com/juju/utils/v3/ssh" 17 18 "github.com/juju/juju/apiserver/common" 19 apiservererrors "github.com/juju/juju/apiserver/errors" 20 "github.com/juju/juju/apiserver/facade" 21 "github.com/juju/juju/core/permission" 22 "github.com/juju/juju/environs/config" 23 "github.com/juju/juju/rpc/params" 24 ) 25 26 var logger = loggo.GetLogger("juju.apiserver.keymanager") 27 28 // The comment values used by juju internal ssh keys. 29 var internalComments = set.NewStrings("juju-client-key", config.JujuSystemKey) 30 31 // KeyManagerAPI provides api endpoints for manipulating ssh keys 32 type KeyManagerAPI struct { 33 model Model 34 authorizer facade.Authorizer 35 check BlockChecker 36 37 controllerTag names.ControllerTag 38 } 39 40 func (api *KeyManagerAPI) checkCanRead(sshUser string) error { 41 if err := api.checkCanWrite(sshUser); err == nil { 42 return nil 43 } else if err != apiservererrors.ErrPerm { 44 return errors.Trace(err) 45 } 46 if sshUser == config.JujuSystemKey { 47 // users cannot read the system key. 48 // NOTE: This check currently has no use as the apiserver ignores the user(s) included 49 // in requests. It exists as an added layer of protection for the future, to prevent users 50 // requesting the system key. Later, when keys are not global we will need to put more 51 // thought into exactly how we should ensure the system key is never exposed to users. 52 // At the moment this is handled by using `internalComments` 53 return apiservererrors.ErrPerm 54 } 55 err := api.authorizer.HasPermission(permission.ReadAccess, api.model.ModelTag()) 56 return err 57 } 58 59 func (api *KeyManagerAPI) checkCanWrite(sshUser string) error { 60 if sshUser == config.JujuSystemKey { 61 // users cannot modify the system key. 62 // NOTE: This check currently has no use as the apiserver ignores the user(s) included 63 // in requests. It exists as an added layer of protection for the future, to prevent users 64 // requesting the system key. Later, when keys are not global we will need to put more 65 // thought into exactly how we should ensure the system key is never exposed to users. 66 // At the moment this is handled by using `internalComments` 67 return apiservererrors.ErrPerm 68 } 69 ok, err := common.HasModelAdmin(api.authorizer, api.controllerTag, api.model.ModelTag()) 70 if err != nil { 71 return errors.Trace(err) 72 } 73 if !ok { 74 return apiservererrors.ErrPerm 75 } 76 return nil 77 } 78 79 // ListKeys returns the authorised ssh keys for the specified users. 80 func (api *KeyManagerAPI) ListKeys(arg params.ListSSHKeys) (params.StringsResults, error) { 81 if len(arg.Entities.Entities) == 0 { 82 return params.StringsResults{}, nil 83 } 84 85 // For now, authorised keys are global, common to all users. 86 cfg, err := api.model.ModelConfig() 87 if err != nil { 88 // Return error embedded in results for compatibility. 89 // TODO: Change this to a call-error on next facade bump 90 results := transform.Slice(arg.Entities.Entities, func(_ params.Entity) params.StringsResult { 91 return params.StringsResult{Error: apiservererrors.ServerError(err)} 92 }) 93 return params.StringsResults{Results: results}, nil 94 } 95 keys := ssh.SplitAuthorisedKeys(cfg.AuthorizedKeys()) 96 keyInfo := parseKeys(keys, arg.Mode) 97 98 results := transform.Slice(arg.Entities.Entities, func(entity params.Entity) params.StringsResult { 99 // NOTE: entity.Tag isn't a tag, but a username. 100 if err := api.checkCanRead(entity.Tag); err != nil { 101 return params.StringsResult{Error: apiservererrors.ServerError(err)} 102 } 103 // All keys are global, no need to look up the user. 104 return params.StringsResult{Result: keyInfo} 105 }) 106 return params.StringsResults{Results: results}, nil 107 } 108 109 func parseKeys(keys []string, mode ssh.ListMode) (keyInfo []string) { 110 for _, key := range keys { 111 fingerprint, comment, err := ssh.KeyFingerprint(key) 112 if err != nil { 113 keyInfo = append(keyInfo, fmt.Sprintf("Invalid key: %v", key)) 114 continue 115 } 116 // Only including user added keys not internal ones. 117 if internalComments.Contains(comment) { 118 continue 119 } 120 if mode == ssh.FullKeys { 121 keyInfo = append(keyInfo, key) 122 } else { 123 shortKey := fingerprint 124 if comment != "" { 125 shortKey += fmt.Sprintf(" (%s)", comment) 126 } 127 keyInfo = append(keyInfo, shortKey) 128 } 129 } 130 return keyInfo 131 } 132 133 func (api *KeyManagerAPI) writeSSHKeys(sshKeys []string) error { 134 // Write out the new keys. 135 keyStr := strings.Join(sshKeys, "\n") 136 attrs := map[string]interface{}{config.AuthorizedKeysKey: keyStr} 137 // TODO(waigani) 2014-03-17 bug #1293324 138 // Pass in validation to ensure SSH keys 139 // have not changed underfoot 140 err := api.model.UpdateModelConfig(attrs, nil) 141 if err != nil { 142 return fmt.Errorf("writing environ config: %v", err) 143 } 144 return nil 145 } 146 147 // currentKeyDataForAdd gathers data used when adding ssh keys. 148 func (api *KeyManagerAPI) currentKeyDataForAdd() (keys []string, fingerprints set.Strings, err error) { 149 fingerprints = make(set.Strings) 150 cfg, err := api.model.ModelConfig() 151 if err != nil { 152 return nil, nil, fmt.Errorf("reading current key data: %v", err) 153 } 154 keys = ssh.SplitAuthorisedKeys(cfg.AuthorizedKeys()) 155 for _, key := range keys { 156 fingerprint, _, err := ssh.KeyFingerprint(key) 157 if err != nil { 158 logger.Warningf("ignoring invalid ssh key %q: %v", key, err) 159 continue 160 } 161 fingerprints.Add(fingerprint) 162 } 163 return keys, fingerprints, nil 164 } 165 166 // AddKeys adds new authorised ssh keys for the specified user. 167 func (api *KeyManagerAPI) AddKeys(arg params.ModifyUserSSHKeys) (params.ErrorResults, error) { 168 if err := api.checkCanWrite(arg.User); err != nil { 169 return params.ErrorResults{}, apiservererrors.ServerError(err) 170 } 171 if err := api.check.ChangeAllowed(); err != nil { 172 return params.ErrorResults{}, errors.Trace(err) 173 } 174 if len(arg.Keys) == 0 { 175 return params.ErrorResults{}, nil 176 } 177 178 // For now, authorised keys are global, common to all users. 179 sshKeys, currentFingerprints, err := api.currentKeyDataForAdd() 180 if err != nil { 181 return params.ErrorResults{}, apiservererrors.ServerError(fmt.Errorf("reading current key data: %v", err)) 182 } 183 184 // Ensure we are not going to add invalid or duplicate keys. 185 results := transform.Slice(arg.Keys, func(key string) params.ErrorResult { 186 fingerprint, comment, err := ssh.KeyFingerprint(key) 187 if err != nil { 188 return params.ErrorResult{Error: apiservererrors.ServerError(fmt.Errorf("invalid ssh key: %s", key))} 189 } 190 if internalComments.Contains(comment) { 191 return params.ErrorResult{Error: apiservererrors.ServerError(fmt.Errorf("may not add key with comment %s: %s", comment, key))} 192 } 193 if currentFingerprints.Contains(fingerprint) { 194 return params.ErrorResult{Error: apiservererrors.ServerError(fmt.Errorf("duplicate ssh key: %s", key))} 195 } 196 currentFingerprints.Add(fingerprint) 197 sshKeys = append(sshKeys, key) 198 return params.ErrorResult{} 199 }) 200 201 err = api.writeSSHKeys(sshKeys) 202 if err != nil { 203 return params.ErrorResults{}, apiservererrors.ServerError(err) 204 } 205 return params.ErrorResults{Results: results}, nil 206 } 207 208 type importedSSHKey struct { 209 key string 210 fingerprint string 211 comment string 212 err error 213 } 214 215 // Override for testing 216 var RunSSHImportId = runSSHImportId 217 218 func runSSHImportId(keyId string) (string, error) { 219 return utils.RunCommand("ssh-import-id", "-o", "-", keyId) 220 } 221 222 // runSSHKeyImport uses ssh-import-id to find the ssh keys for the specified key ids. 223 func runSSHKeyImport(keyIds []string) map[string][]importedSSHKey { 224 importResults := make(map[string][]importedSSHKey, len(keyIds)) 225 for _, keyId := range keyIds { 226 keyInfo := []importedSSHKey{} 227 output, err := RunSSHImportId(keyId) 228 if err != nil { 229 keyInfo = append(keyInfo, importedSSHKey{err: err}) 230 importResults[keyId] = keyInfo 231 continue 232 } 233 lines := strings.Split(output, "\n") 234 hasKey := false 235 for _, line := range lines { 236 if !strings.HasPrefix(line, "ssh-") { 237 continue 238 } 239 hasKey = true 240 fingerprint, comment, err := ssh.KeyFingerprint(line) 241 keyInfo = append(keyInfo, importedSSHKey{ 242 key: line, 243 fingerprint: fingerprint, 244 comment: comment, 245 err: errors.Annotatef(err, "invalid ssh key for %s", keyId), 246 }) 247 } 248 if !hasKey { 249 keyInfo = append(keyInfo, importedSSHKey{ 250 err: errors.Errorf("invalid ssh key id: %s", keyId), 251 }) 252 } 253 importResults[keyId] = keyInfo 254 } 255 return importResults 256 } 257 258 // ImportKeys imports new authorised ssh keys from the specified key ids for the specified user. 259 func (api *KeyManagerAPI) ImportKeys(arg params.ModifyUserSSHKeys) (params.ErrorResults, error) { 260 if err := api.checkCanWrite(arg.User); err != nil { 261 return params.ErrorResults{}, apiservererrors.ServerError(err) 262 } 263 if err := api.check.ChangeAllowed(); err != nil { 264 return params.ErrorResults{}, errors.Trace(err) 265 } 266 if len(arg.Keys) == 0 { 267 return params.ErrorResults{}, nil 268 } 269 270 // For now, authorised keys are global, common to all users. 271 sshKeys, currentFingerprints, err := api.currentKeyDataForAdd() 272 if err != nil { 273 return params.ErrorResults{}, apiservererrors.ServerError(fmt.Errorf("reading current key data: %v", err)) 274 } 275 276 importedKeyInfo := runSSHKeyImport(arg.Keys) 277 278 // Ensure we are not going to add invalid or duplicate keys. 279 results := transform.Slice(arg.Keys, func(key string) params.ErrorResult { 280 compoundErr := "" 281 for _, keyInfo := range importedKeyInfo[key] { 282 if keyInfo.err != nil { 283 compoundErr += fmt.Sprintf("%v\n", keyInfo.err) 284 continue 285 } 286 if internalComments.Contains(keyInfo.comment) { 287 compoundErr += fmt.Sprintf("%v\n", errors.Errorf("may not add key with comment %s: %s", keyInfo.comment, keyInfo.key)) 288 continue 289 } 290 if currentFingerprints.Contains(keyInfo.fingerprint) { 291 compoundErr += fmt.Sprintf("%v\n", errors.Errorf("duplicate ssh key: %s", keyInfo.key)) 292 continue 293 } 294 sshKeys = append(sshKeys, keyInfo.key) 295 } 296 if compoundErr != "" { 297 return params.ErrorResult{Error: apiservererrors.ServerError(errors.Errorf(strings.TrimSuffix(compoundErr, "\n")))} 298 } 299 return params.ErrorResult{} 300 }) 301 302 err = api.writeSSHKeys(sshKeys) 303 if err != nil { 304 return params.ErrorResults{}, apiservererrors.ServerError(err) 305 } 306 return params.ErrorResults{Results: results}, nil 307 } 308 309 type keyDataForDelete struct { 310 allKeys []string 311 byFingerprint map[string]string 312 byComment map[string]string 313 invalidKeys map[string]string 314 } 315 316 // currentKeyDataForDelete gathers data used when deleting ssh keys. 317 func (api *KeyManagerAPI) currentKeyDataForDelete() (keyDataForDelete, error) { 318 319 cfg, err := api.model.ModelConfig() 320 if err != nil { 321 return keyDataForDelete{}, fmt.Errorf("reading current key data: %v", err) 322 } 323 // For now, authorised keys are global, common to all users. 324 currentKeys := ssh.SplitAuthorisedKeys(cfg.AuthorizedKeys()) 325 326 // Make two maps that index keys by fingerprint and by comment for fast 327 // lookup of keys to delete which may be given as either. 328 byFingerprint := make(map[string]string) 329 byComment := make(map[string]string) 330 invalidKeys := make(map[string]string) 331 for _, key := range currentKeys { 332 fingerprint, comment, err := ssh.KeyFingerprint(key) 333 if err != nil { 334 logger.Debugf("invalid existing ssh key %q: %v", key, err) 335 invalidKeys[key] = key 336 continue 337 } 338 byFingerprint[fingerprint] = key 339 if comment != "" { 340 byComment[comment] = key 341 } 342 } 343 data := keyDataForDelete{ 344 allKeys: currentKeys, 345 byFingerprint: byFingerprint, 346 byComment: byComment, 347 invalidKeys: invalidKeys, 348 } 349 return data, nil 350 } 351 352 // DeleteKeys deletes the authorised ssh keys for the specified user. 353 func (api *KeyManagerAPI) DeleteKeys(arg params.ModifyUserSSHKeys) (params.ErrorResults, error) { 354 if err := api.checkCanWrite(arg.User); err != nil { 355 return params.ErrorResults{}, apiservererrors.ServerError(err) 356 } 357 if err := api.check.RemoveAllowed(); err != nil { 358 return params.ErrorResults{}, errors.Trace(err) 359 } 360 if len(arg.Keys) == 0 { 361 return params.ErrorResults{}, nil 362 } 363 364 keyData, err := api.currentKeyDataForDelete() 365 if err != nil { 366 return params.ErrorResults{}, apiservererrors.ServerError(fmt.Errorf("reading current key data: %v", err)) 367 } 368 369 // Record the keys to be deleted in the second pass. 370 keysToDelete := make(set.Strings) 371 372 results := transform.Slice(arg.Keys, func(keyId string) params.ErrorResult { 373 // Is given keyId a fingerprint? 374 key, ok := keyData.byFingerprint[keyId] 375 if ok { 376 keysToDelete.Add(key) 377 return params.ErrorResult{} 378 } 379 // Not a fingerprint, is it a comment? 380 key, ok = keyData.byComment[keyId] 381 if ok { 382 if internalComments.Contains(keyId) { 383 return params.ErrorResult{Error: apiservererrors.ServerError(fmt.Errorf("may not delete internal key: %s", keyId))} 384 } 385 keysToDelete.Add(key) 386 return params.ErrorResult{} 387 } 388 // Allow invalid keys to be deleted by writing out key verbatim. 389 key, ok = keyData.invalidKeys[keyId] 390 if ok { 391 keysToDelete.Add(key) 392 return params.ErrorResult{} 393 } 394 return params.ErrorResult{Error: apiservererrors.ServerError(fmt.Errorf("key not found: %s", keyId))} 395 }) 396 397 var keysToWrite []string 398 399 // Add back only the keys that are not deleted, preserving the order. 400 for _, key := range keyData.allKeys { 401 if !keysToDelete.Contains(key) { 402 keysToWrite = append(keysToWrite, key) 403 } 404 } 405 406 if len(keysToWrite) == 0 { 407 return params.ErrorResults{}, apiservererrors.ServerError(fmt.Errorf("cannot delete all keys")) 408 } 409 410 err = api.writeSSHKeys(keysToWrite) 411 if err != nil { 412 return params.ErrorResults{}, apiservererrors.ServerError(err) 413 } 414 return params.ErrorResults{Results: results}, nil 415 }