diff --git a/README.md b/README.md index 49a03e39..e1ae9d78 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Available Commands: Flags: --app-id string The GitHub App to connect to. ($BATON_APP_ID) - --app-privatekey string Raw PEM contents of the private key used to connect to the GitHub App. Takes precedence over app-privatekey-path when both are set. ($BATON_APP_PRIVATEKEY) + --app-privatekey string Raw PEM contents of the private key used to connect to the GitHub App. Takes precedence over app-privatekey-path when both are set. Literal \n escape sequences are also accepted and unescaped before use, since this field can't hold newlines when entered through a form. ($BATON_APP_PRIVATEKEY) --app-privatekey-path string Path to private key that is used to connect to the GitHub App. Ignored when app-privatekey is set. ($BATON_APP_PRIVATEKEY_PATH) --client-id string The client ID used to authenticate with ConductorOne ($BATON_CLIENT_ID) --client-secret string The client secret used to authenticate with ConductorOne ($BATON_CLIENT_SECRET) diff --git a/config_schema.json b/config_schema.json index 035f781f..6b0dce9b 100644 --- a/config_schema.json +++ b/config_schema.json @@ -148,7 +148,7 @@ { "name": "app-privatekey", "displayName": "GitHub App private key (PEM)", - "description": "Raw PEM contents of the private key used to connect to the GitHub App. Takes precedence over app-privatekey-path when both are set.", + "description": "Raw PEM contents of the private key used to connect to the GitHub App. Takes precedence over app-privatekey-path when both are set. Literal \\n escape sequences are also accepted and unescaped before use, since this field can't hold newlines when entered through a form.", "isSecret": true, "stringField": {} }, diff --git a/docs/connector.mdx b/docs/connector.mdx index 12e96d0a..b44aacb2 100644 --- a/docs/connector.mdx +++ b/docs/connector.mdx @@ -301,7 +301,9 @@ If you're using a GitHub app to set up the connector: 1. Enter your app ID into the **GitHub app ID** field. - 1. Click **Choose file** and upload your private key file. + 1. Provide the GitHub App's private key one of two ways: + - Click **Choose file** and upload your private key (`.pem`) file, or + - Paste the key's raw PEM contents into the **GitHub App private key (PEM)** field. Because this field only accepts a single line of text, replace every line break in the PEM with a literal `\n` (backslash, then the letter `n`) before pasting it in — the connector unescapes these back into real line breaks. If both fields are filled in, the pasted **GitHub App private key (PEM)** value takes precedence. 1. In the **Organization** field, enter the name of the GitHub organization associated with the GitHub app. **You must enter a single organization name in this field or the connector configuration will fail.** diff --git a/pkg/config/config.go b/pkg/config/config.go index 5c676fac..edee4c86 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -57,7 +57,10 @@ var ( appPrivateKey = field.StringField( "app-privatekey", field.WithDisplayName("GitHub App private key (PEM)"), - field.WithDescription("Raw PEM contents of the private key used to connect to the GitHub App. Takes precedence over app-privatekey-path when both are set."), + field.WithDescription( + "Raw PEM contents of the private key used to connect to the GitHub App. Takes precedence over app-privatekey-path when both are set. "+ + `Literal \n escape sequences are also accepted and unescaped before use, since this field can't hold newlines when entered through a form.`, + ), field.WithIsSecret(true), ) diff --git a/pkg/connector/app_privatekey_test.go b/pkg/connector/app_privatekey_test.go index 3e455fdc..5d791aba 100644 --- a/pkg/connector/app_privatekey_test.go +++ b/pkg/connector/app_privatekey_test.go @@ -1,6 +1,11 @@ package connector import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "strings" "testing" cfg "github.com/conductorone/baton-github/pkg/config" @@ -39,3 +44,33 @@ func TestAppPrivateKeyPEM(t *testing.T) { require.Error(t, err) }) } + +func TestLoadPrivateKeyFromString(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + der := x509.MarshalPKCS1PrivateKey(key) + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: der}) + realNewlines := string(pemBytes) + + shapes := map[string]string{ + "real newlines": realNewlines, + "real CRLF": strings.ReplaceAll(realNewlines, "\n", "\r\n"), + "backslash-n escaped, as the docs instruct": strings.ReplaceAll(realNewlines, "\n", `\n`), + "backslash-n escaped, trailing space": strings.ReplaceAll(realNewlines, "\n", `\n`) + " ", + "backslash-r-backslash-n escaped (CRLF file)": strings.ReplaceAll(realNewlines, "\n", `\r\n`), + "backslash-r escaped (CR-only file)": strings.ReplaceAll(realNewlines, "\n", `\r`), + } + + for name, shape := range shapes { + t.Run("parses a PEM with "+name, func(t *testing.T) { + got, err := loadPrivateKeyFromString(shape) + require.NoError(t, err) + require.Equal(t, key.D, got.D) + }) + } + + t.Run("errors on garbage input", func(t *testing.T) { + _, err := loadPrivateKeyFromString("not a pem") + require.Error(t, err) + }) +} diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index b38ed4e8..76577f5d 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -484,7 +484,12 @@ func newGitHubGraphqlClient(ctx context.Context, instanceURL string, ts oauth2.T return githubv4.NewClient(tc), nil } +// escapedLineBreaks unescapes LF-, CRLF-, and CR-escaped line breaks (`\r\n`, +// `\n`, `\r`) to a real newline. +var escapedLineBreaks = strings.NewReplacer(`\r\n`, "\n", `\n`, "\n", `\r`, "\n") + func loadPrivateKeyFromString(p string) (*rsa.PrivateKey, error) { + p = escapedLineBreaks.Replace(p) block, _ := pem.Decode([]byte(p)) if block == nil || (block.Type != "PRIVATE KEY" && block.Type != "RSA PRIVATE KEY") { return nil, errors.New("invalid private key PEM format")