provider.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. package processcreds
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "os"
  9. "os/exec"
  10. "runtime"
  11. "time"
  12. "github.com/aws/aws-sdk-go-v2/aws"
  13. "github.com/aws/aws-sdk-go-v2/internal/sdkio"
  14. )
  15. const (
  16. // ProviderName is the name this credentials provider will label any
  17. // returned credentials Value with.
  18. ProviderName = `ProcessProvider`
  19. // DefaultTimeout default limit on time a process can run.
  20. DefaultTimeout = time.Duration(1) * time.Minute
  21. )
  22. // ProviderError is an error indicating failure initializing or executing the
  23. // process credentials provider
  24. type ProviderError struct {
  25. Err error
  26. }
  27. // Error returns the error message.
  28. func (e *ProviderError) Error() string {
  29. return fmt.Sprintf("process provider error: %v", e.Err)
  30. }
  31. // Unwrap returns the underlying error the provider error wraps.
  32. func (e *ProviderError) Unwrap() error {
  33. return e.Err
  34. }
  35. // Provider satisfies the credentials.Provider interface, and is a
  36. // client to retrieve credentials from a process.
  37. type Provider struct {
  38. // Provides a constructor for exec.Cmd that are invoked by the provider for
  39. // retrieving credentials. Use this to provide custom creation of exec.Cmd
  40. // with things like environment variables, or other configuration.
  41. //
  42. // The provider defaults to the DefaultNewCommand function.
  43. commandBuilder NewCommandBuilder
  44. options Options
  45. }
  46. // Options is the configuration options for configuring the Provider.
  47. type Options struct {
  48. // Timeout limits the time a process can run.
  49. Timeout time.Duration
  50. // The chain of providers that was used to create this provider
  51. // These values are for reporting purposes and are not meant to be set up directly
  52. CredentialSources []aws.CredentialSource
  53. }
  54. // NewCommandBuilder provides the interface for specifying how command will be
  55. // created that the Provider will use to retrieve credentials with.
  56. type NewCommandBuilder interface {
  57. NewCommand(context.Context) (*exec.Cmd, error)
  58. }
  59. // NewCommandBuilderFunc provides a wrapper type around a function pointer to
  60. // satisfy the NewCommandBuilder interface.
  61. type NewCommandBuilderFunc func(context.Context) (*exec.Cmd, error)
  62. // NewCommand calls the underlying function pointer the builder was initialized with.
  63. func (fn NewCommandBuilderFunc) NewCommand(ctx context.Context) (*exec.Cmd, error) {
  64. return fn(ctx)
  65. }
  66. // DefaultNewCommandBuilder provides the default NewCommandBuilder
  67. // implementation used by the provider. It takes a command and arguments to
  68. // invoke. The command will also be initialized with the current process
  69. // environment variables, stderr, and stdin pipes.
  70. type DefaultNewCommandBuilder struct {
  71. Args []string
  72. }
  73. // NewCommand returns an initialized exec.Cmd with the builder's initialized
  74. // Args. The command is also initialized current process environment variables,
  75. // stderr, and stdin pipes.
  76. func (b DefaultNewCommandBuilder) NewCommand(ctx context.Context) (*exec.Cmd, error) {
  77. var cmdArgs []string
  78. if runtime.GOOS == "windows" {
  79. cmdArgs = []string{"cmd.exe", "/C"}
  80. } else {
  81. cmdArgs = []string{"sh", "-c"}
  82. }
  83. if len(b.Args) == 0 {
  84. return nil, &ProviderError{
  85. Err: fmt.Errorf("failed to prepare command: command must not be empty"),
  86. }
  87. }
  88. cmdArgs = append(cmdArgs, b.Args...)
  89. cmd := exec.CommandContext(ctx, cmdArgs[0], cmdArgs[1:]...)
  90. cmd.Env = os.Environ()
  91. cmd.Stderr = os.Stderr // display stderr on console for MFA
  92. cmd.Stdin = os.Stdin // enable stdin for MFA
  93. return cmd, nil
  94. }
  95. // NewProvider returns a pointer to a new Credentials object wrapping the
  96. // Provider.
  97. //
  98. // The provider defaults to the DefaultNewCommandBuilder for creating command
  99. // the Provider will use to retrieve credentials with.
  100. func NewProvider(command string, options ...func(*Options)) *Provider {
  101. var args []string
  102. // Ensure that the command arguments are not set if the provided command is
  103. // empty. This will error out when the command is executed since no
  104. // arguments are specified.
  105. if len(command) > 0 {
  106. args = []string{command}
  107. }
  108. commanBuilder := DefaultNewCommandBuilder{
  109. Args: args,
  110. }
  111. return NewProviderCommand(commanBuilder, options...)
  112. }
  113. // NewProviderCommand returns a pointer to a new Credentials object with the
  114. // specified command, and default timeout duration. Use this to provide custom
  115. // creation of exec.Cmd for options like environment variables, or other
  116. // configuration.
  117. func NewProviderCommand(builder NewCommandBuilder, options ...func(*Options)) *Provider {
  118. p := &Provider{
  119. commandBuilder: builder,
  120. options: Options{
  121. Timeout: DefaultTimeout,
  122. },
  123. }
  124. for _, option := range options {
  125. option(&p.options)
  126. }
  127. return p
  128. }
  129. // A CredentialProcessResponse is the AWS credentials format that must be
  130. // returned when executing an external credential_process.
  131. type CredentialProcessResponse struct {
  132. // As of this writing, the Version key must be set to 1. This might
  133. // increment over time as the structure evolves.
  134. Version int
  135. // The access key ID that identifies the temporary security credentials.
  136. AccessKeyID string `json:"AccessKeyId"`
  137. // The secret access key that can be used to sign requests.
  138. SecretAccessKey string
  139. // The token that users must pass to the service API to use the temporary credentials.
  140. SessionToken string
  141. // The date on which the current credentials expire.
  142. Expiration *time.Time
  143. // The ID of the account for credentials
  144. AccountID string `json:"AccountId"`
  145. }
  146. // Retrieve executes the credential process command and returns the
  147. // credentials, or error if the command fails.
  148. func (p *Provider) Retrieve(ctx context.Context) (aws.Credentials, error) {
  149. out, err := p.executeCredentialProcess(ctx)
  150. if err != nil {
  151. return aws.Credentials{Source: ProviderName}, err
  152. }
  153. // Serialize and validate response
  154. resp := &CredentialProcessResponse{}
  155. if err = json.Unmarshal(out, resp); err != nil {
  156. return aws.Credentials{Source: ProviderName}, &ProviderError{
  157. Err: fmt.Errorf("parse failed of process output: %s, error: %w", out, err),
  158. }
  159. }
  160. if resp.Version != 1 {
  161. return aws.Credentials{Source: ProviderName}, &ProviderError{
  162. Err: fmt.Errorf("wrong version in process output (not 1)"),
  163. }
  164. }
  165. if len(resp.AccessKeyID) == 0 {
  166. return aws.Credentials{Source: ProviderName}, &ProviderError{
  167. Err: fmt.Errorf("missing AccessKeyId in process output"),
  168. }
  169. }
  170. if len(resp.SecretAccessKey) == 0 {
  171. return aws.Credentials{Source: ProviderName}, &ProviderError{
  172. Err: fmt.Errorf("missing SecretAccessKey in process output"),
  173. }
  174. }
  175. creds := aws.Credentials{
  176. Source: ProviderName,
  177. AccessKeyID: resp.AccessKeyID,
  178. SecretAccessKey: resp.SecretAccessKey,
  179. SessionToken: resp.SessionToken,
  180. AccountID: resp.AccountID,
  181. }
  182. // Handle expiration
  183. if resp.Expiration != nil {
  184. creds.CanExpire = true
  185. creds.Expires = *resp.Expiration
  186. }
  187. return creds, nil
  188. }
  189. // executeCredentialProcess starts the credential process on the OS and
  190. // returns the results or an error.
  191. func (p *Provider) executeCredentialProcess(ctx context.Context) ([]byte, error) {
  192. if p.options.Timeout >= 0 {
  193. var cancelFunc func()
  194. ctx, cancelFunc = context.WithTimeout(ctx, p.options.Timeout)
  195. defer cancelFunc()
  196. }
  197. cmd, err := p.commandBuilder.NewCommand(ctx)
  198. if err != nil {
  199. return nil, err
  200. }
  201. // get creds json on process's stdout
  202. output := bytes.NewBuffer(make([]byte, 0, int(8*sdkio.KibiByte)))
  203. if cmd.Stdout != nil {
  204. cmd.Stdout = io.MultiWriter(cmd.Stdout, output)
  205. } else {
  206. cmd.Stdout = output
  207. }
  208. execCh := make(chan error, 1)
  209. go executeCommand(cmd, execCh)
  210. select {
  211. case execError := <-execCh:
  212. if execError == nil {
  213. break
  214. }
  215. select {
  216. case <-ctx.Done():
  217. return output.Bytes(), &ProviderError{
  218. Err: fmt.Errorf("credential process timed out: %w", execError),
  219. }
  220. default:
  221. return output.Bytes(), &ProviderError{
  222. Err: fmt.Errorf("error in credential_process: %w", execError),
  223. }
  224. }
  225. }
  226. out := output.Bytes()
  227. if runtime.GOOS == "windows" {
  228. // windows adds slashes to quotes
  229. out = bytes.ReplaceAll(out, []byte(`\"`), []byte(`"`))
  230. }
  231. return out, nil
  232. }
  233. // ProviderSources returns the credential chain that was used to construct this provider
  234. func (p *Provider) ProviderSources() []aws.CredentialSource {
  235. if p.options.CredentialSources == nil {
  236. return []aws.CredentialSource{aws.CredentialSourceProcess}
  237. }
  238. return p.options.CredentialSources
  239. }
  240. func executeCommand(cmd *exec.Cmd, exec chan error) {
  241. // Start the command
  242. err := cmd.Start()
  243. if err == nil {
  244. err = cmd.Wait()
  245. }
  246. exec <- err
  247. }