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  }