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 }