github.com/wtfutil/wtf@v0.43.0/cfg/secrets.go (about)

     1  package cfg
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"runtime"
     7  
     8  	"github.com/docker/docker-credential-helpers/client"
     9  	"github.com/docker/docker-credential-helpers/credentials"
    10  	"github.com/olebedev/config"
    11  	"github.com/wtfutil/wtf/logger"
    12  )
    13  
    14  type SecretLoadParams struct {
    15  	name         string
    16  	globalConfig *config.Config
    17  	service      string
    18  
    19  	secret *string
    20  }
    21  
    22  // Load module secrets.
    23  //
    24  // The credential helpers impose this structure:
    25  //
    26  //	SERVICE is mapped to a SECRET and USERNAME
    27  //
    28  // Only SECRET is secret, SERVICE and USERNAME are not, so this
    29  // API doesn't expose USERNAME.
    30  //
    31  // SERVICE was intended to be the URL of an API server, but
    32  // for hosted services that do not have or need a configurable
    33  // API server, its easier to just use the module name as the
    34  // SERVICE:
    35  //
    36  //	   cfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()
    37  //
    38  //	The user will use the module name as the service, and the API key as
    39  //	the secret, for example:
    40  //
    41  //	   % wtfutil save-secret circleci
    42  //	   Secret: ...
    43  //
    44  // If a module (such as pihole, jenkins, or github) might have multiple
    45  // instantiations each using a different API service (with its own unique
    46  // API key), then the module should use the API URL to lookup the secret.
    47  // For example, for github:
    48  //
    49  //	cfg.ModuleSecret(name, globalConfig, &settings.apiKey).
    50  //	    Service(settings.baseURL).
    51  //	    Load()
    52  //
    53  // The user will use the API URL as the service, and the API key as the
    54  // secret, for example, with github configured as:
    55  //
    56  //	   -- config.yml
    57  //	   mods:
    58  //	     github:
    59  //	       baseURL: "https://github.mycompany.com/api/v3"
    60  //	       ...
    61  //
    62  //	the secret must be saved as:
    63  //
    64  //	   % wtfutil save-secret https://github.mycompany.com/api/v3
    65  //	   Secret: ...
    66  //
    67  //	If baseURL is not set in the configuration it will be the modules
    68  //	default, and the SERVICE will default to the module name, "github",
    69  //	and the user must save the secret as:
    70  //
    71  //	   % wtfutil save-secret github
    72  //	   Secret: ...
    73  //
    74  //	Ideally, the individual module documentation would describe the
    75  //	SERVICE name to use to save the secret.
    76  func ModuleSecret(name string, globalConfig *config.Config, secret *string) *SecretLoadParams {
    77  	return &SecretLoadParams{
    78  		name:         name,
    79  		globalConfig: globalConfig,
    80  		secret:       secret,
    81  		service:      name, // Default the service to the module name
    82  	}
    83  }
    84  
    85  func (slp *SecretLoadParams) Service(service string) *SecretLoadParams {
    86  	if service != "" {
    87  		slp.service = service
    88  	}
    89  	return slp
    90  }
    91  
    92  func (slp *SecretLoadParams) Load() {
    93  	configureSecret(
    94  		slp.globalConfig,
    95  		slp.service,
    96  		slp.secret,
    97  	)
    98  }
    99  
   100  type Secret struct {
   101  	Service  string
   102  	Secret   string
   103  	Username string
   104  	Store    string
   105  }
   106  
   107  func configureSecret(
   108  	globalConfig *config.Config,
   109  	service string,
   110  	secret *string,
   111  ) {
   112  	if service == "" {
   113  		return
   114  	}
   115  
   116  	if secret == nil {
   117  		return
   118  	}
   119  
   120  	// Don't overwrite the secret if it was configured with yaml
   121  	if *secret != "" {
   122  		return
   123  	}
   124  
   125  	cred, err := FetchSecret(globalConfig, service)
   126  
   127  	if err != nil {
   128  		logger.Log(fmt.Sprintf("Loading secret failed: %s", err.Error()))
   129  		return
   130  	}
   131  
   132  	if cred == nil {
   133  		// No secret store configued.
   134  		return
   135  	}
   136  
   137  	if secret != nil && *secret == "" {
   138  		*secret = cred.Secret
   139  	}
   140  }
   141  
   142  // Fetch secret for `service`. Service is customarily a URL, but can be any
   143  // identifier uniquely used by wtf to identify the service, such as the name
   144  // of the module.  nil is returned if the secretStore global property is not
   145  // present or the secret is not found in that store.
   146  func FetchSecret(globalConfig *config.Config, service string) (*Secret, error) {
   147  	prog := newProgram(globalConfig)
   148  
   149  	if prog == nil {
   150  		// No secret store configured.
   151  		return nil, nil
   152  	}
   153  
   154  	cred, err := client.Get(prog.runner, service)
   155  
   156  	if err != nil {
   157  		return nil, fmt.Errorf("get %v from %v: %w", service, prog.store, err)
   158  	}
   159  
   160  	return &Secret{
   161  		Service:  cred.ServerURL,
   162  		Secret:   cred.Secret,
   163  		Username: cred.Username,
   164  		Store:    prog.store,
   165  	}, nil
   166  }
   167  
   168  func StoreSecret(globalConfig *config.Config, secret *Secret) error {
   169  	prog := newProgram(globalConfig)
   170  
   171  	if prog == nil {
   172  		return errors.New("cannot store secrets: wtf.secretStore is not configured")
   173  	}
   174  
   175  	cred := &credentials.Credentials{
   176  		ServerURL: secret.Service,
   177  		Username:  secret.Username,
   178  		Secret:    secret.Secret,
   179  	}
   180  
   181  	// docker-credential requires a username, but it isn't necessary for
   182  	// all services. Use a default if a username was not set.
   183  	if cred.Username == "" {
   184  		cred.Username = "default"
   185  	}
   186  
   187  	err := client.Store(prog.runner, cred)
   188  
   189  	if err != nil {
   190  		return fmt.Errorf("store %v: %w", prog.store, err)
   191  	}
   192  
   193  	return nil
   194  }
   195  
   196  type program struct {
   197  	store  string
   198  	runner client.ProgramFunc
   199  }
   200  
   201  func newProgram(globalConfig *config.Config) *program {
   202  	secretStore := globalConfig.UString("wtf.secretStore", "(none)")
   203  
   204  	if secretStore == "(none)" {
   205  		return nil
   206  	}
   207  
   208  	if secretStore == "" {
   209  		switch runtime.GOOS {
   210  		case "windows":
   211  			secretStore = "winrt"
   212  		case "darwin":
   213  			secretStore = "osxkeychain"
   214  		default:
   215  			secretStore = "secretservice"
   216  		}
   217  
   218  	}
   219  
   220  	return &program{
   221  		secretStore,
   222  		client.NewShellProgramFunc("docker-credential-" + secretStore),
   223  	}
   224  }