diff --git a/contrib/completions/bash/oadm b/contrib/completions/bash/oadm index c02026104ac2..aa520b763e40 100644 --- a/contrib/completions/bash/oadm +++ b/contrib/completions/bash/oadm @@ -2978,6 +2978,105 @@ _oadm_ca_create-signer-cert() must_have_one_noun=() } +_oadm_ca_encrypt() +{ + last_command="oadm_ca_encrypt" + commands=() + + flags=() + two_word_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--genkey=") + flags_with_completion+=("--genkey") + flags_completion+=("_filedir") + flags+=("--in=") + flags_with_completion+=("--in") + flags_completion+=("_filedir") + flags+=("--key=") + flags_with_completion+=("--key") + flags_completion+=("_filedir") + flags+=("--out=") + flags_with_completion+=("--out") + flags_completion+=("_filedir") + flags+=("--api-version=") + flags+=("--certificate-authority=") + flags_with_completion+=("--certificate-authority") + flags_completion+=("_filedir") + flags+=("--client-certificate=") + flags_with_completion+=("--client-certificate") + flags_completion+=("_filedir") + flags+=("--client-key=") + flags_with_completion+=("--client-key") + flags_completion+=("_filedir") + flags+=("--cluster=") + flags+=("--config=") + flags_with_completion+=("--config") + flags_completion+=("_filedir") + flags+=("--context=") + flags+=("--google-json-key=") + flags+=("--insecure-skip-tls-verify") + flags+=("--log-flush-frequency=") + flags+=("--match-server-version") + flags+=("--namespace=") + two_word_flags+=("-n") + flags+=("--server=") + flags+=("--token=") + flags+=("--user=") + + must_have_one_flag=() + must_have_one_noun=() +} + +_oadm_ca_decrypt() +{ + last_command="oadm_ca_decrypt" + commands=() + + flags=() + two_word_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--in=") + flags_with_completion+=("--in") + flags_completion+=("_filedir") + flags+=("--key=") + flags_with_completion+=("--key") + flags_completion+=("_filedir") + flags+=("--out=") + flags_with_completion+=("--out") + flags_completion+=("_filedir") + flags+=("--api-version=") + flags+=("--certificate-authority=") + flags_with_completion+=("--certificate-authority") + flags_completion+=("_filedir") + flags+=("--client-certificate=") + flags_with_completion+=("--client-certificate") + flags_completion+=("_filedir") + flags+=("--client-key=") + flags_with_completion+=("--client-key") + flags_completion+=("_filedir") + flags+=("--cluster=") + flags+=("--config=") + flags_with_completion+=("--config") + flags_completion+=("_filedir") + flags+=("--context=") + flags+=("--google-json-key=") + flags+=("--insecure-skip-tls-verify") + flags+=("--log-flush-frequency=") + flags+=("--match-server-version") + flags+=("--namespace=") + two_word_flags+=("-n") + flags+=("--server=") + flags+=("--token=") + flags+=("--user=") + + must_have_one_flag=() + must_have_one_noun=() +} + _oadm_ca() { last_command="oadm_ca" @@ -2986,6 +3085,8 @@ _oadm_ca() commands+=("create-key-pair") commands+=("create-server-cert") commands+=("create-signer-cert") + commands+=("encrypt") + commands+=("decrypt") flags=() two_word_flags=() diff --git a/contrib/completions/bash/oc b/contrib/completions/bash/oc index cd12ee8356c3..816356514772 100644 --- a/contrib/completions/bash/oc +++ b/contrib/completions/bash/oc @@ -5714,6 +5714,105 @@ _oc_adm_ca_create-signer-cert() must_have_one_noun=() } +_oc_adm_ca_encrypt() +{ + last_command="oc_adm_ca_encrypt" + commands=() + + flags=() + two_word_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--genkey=") + flags_with_completion+=("--genkey") + flags_completion+=("_filedir") + flags+=("--in=") + flags_with_completion+=("--in") + flags_completion+=("_filedir") + flags+=("--key=") + flags_with_completion+=("--key") + flags_completion+=("_filedir") + flags+=("--out=") + flags_with_completion+=("--out") + flags_completion+=("_filedir") + flags+=("--api-version=") + flags+=("--certificate-authority=") + flags_with_completion+=("--certificate-authority") + flags_completion+=("_filedir") + flags+=("--client-certificate=") + flags_with_completion+=("--client-certificate") + flags_completion+=("_filedir") + flags+=("--client-key=") + flags_with_completion+=("--client-key") + flags_completion+=("_filedir") + flags+=("--cluster=") + flags+=("--config=") + flags_with_completion+=("--config") + flags_completion+=("_filedir") + flags+=("--context=") + flags+=("--google-json-key=") + flags+=("--insecure-skip-tls-verify") + flags+=("--log-flush-frequency=") + flags+=("--match-server-version") + flags+=("--namespace=") + two_word_flags+=("-n") + flags+=("--server=") + flags+=("--token=") + flags+=("--user=") + + must_have_one_flag=() + must_have_one_noun=() +} + +_oc_adm_ca_decrypt() +{ + last_command="oc_adm_ca_decrypt" + commands=() + + flags=() + two_word_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--in=") + flags_with_completion+=("--in") + flags_completion+=("_filedir") + flags+=("--key=") + flags_with_completion+=("--key") + flags_completion+=("_filedir") + flags+=("--out=") + flags_with_completion+=("--out") + flags_completion+=("_filedir") + flags+=("--api-version=") + flags+=("--certificate-authority=") + flags_with_completion+=("--certificate-authority") + flags_completion+=("_filedir") + flags+=("--client-certificate=") + flags_with_completion+=("--client-certificate") + flags_completion+=("_filedir") + flags+=("--client-key=") + flags_with_completion+=("--client-key") + flags_completion+=("_filedir") + flags+=("--cluster=") + flags+=("--config=") + flags_with_completion+=("--config") + flags_completion+=("_filedir") + flags+=("--context=") + flags+=("--google-json-key=") + flags+=("--insecure-skip-tls-verify") + flags+=("--log-flush-frequency=") + flags+=("--match-server-version") + flags+=("--namespace=") + two_word_flags+=("-n") + flags+=("--server=") + flags+=("--token=") + flags+=("--user=") + + must_have_one_flag=() + must_have_one_noun=() +} + _oc_adm_ca() { last_command="oc_adm_ca" @@ -5722,6 +5821,8 @@ _oc_adm_ca() commands+=("create-key-pair") commands+=("create-server-cert") commands+=("create-signer-cert") + commands+=("encrypt") + commands+=("decrypt") flags=() two_word_flags=() diff --git a/contrib/completions/bash/openshift b/contrib/completions/bash/openshift index 55190d8206ba..7967afa4e3f0 100644 --- a/contrib/completions/bash/openshift +++ b/contrib/completions/bash/openshift @@ -3533,6 +3533,105 @@ _openshift_admin_ca_create-signer-cert() must_have_one_noun=() } +_openshift_admin_ca_encrypt() +{ + last_command="openshift_admin_ca_encrypt" + commands=() + + flags=() + two_word_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--genkey=") + flags_with_completion+=("--genkey") + flags_completion+=("_filedir") + flags+=("--in=") + flags_with_completion+=("--in") + flags_completion+=("_filedir") + flags+=("--key=") + flags_with_completion+=("--key") + flags_completion+=("_filedir") + flags+=("--out=") + flags_with_completion+=("--out") + flags_completion+=("_filedir") + flags+=("--api-version=") + flags+=("--certificate-authority=") + flags_with_completion+=("--certificate-authority") + flags_completion+=("_filedir") + flags+=("--client-certificate=") + flags_with_completion+=("--client-certificate") + flags_completion+=("_filedir") + flags+=("--client-key=") + flags_with_completion+=("--client-key") + flags_completion+=("_filedir") + flags+=("--cluster=") + flags+=("--config=") + flags_with_completion+=("--config") + flags_completion+=("_filedir") + flags+=("--context=") + flags+=("--google-json-key=") + flags+=("--insecure-skip-tls-verify") + flags+=("--log-flush-frequency=") + flags+=("--match-server-version") + flags+=("--namespace=") + two_word_flags+=("-n") + flags+=("--server=") + flags+=("--token=") + flags+=("--user=") + + must_have_one_flag=() + must_have_one_noun=() +} + +_openshift_admin_ca_decrypt() +{ + last_command="openshift_admin_ca_decrypt" + commands=() + + flags=() + two_word_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--in=") + flags_with_completion+=("--in") + flags_completion+=("_filedir") + flags+=("--key=") + flags_with_completion+=("--key") + flags_completion+=("_filedir") + flags+=("--out=") + flags_with_completion+=("--out") + flags_completion+=("_filedir") + flags+=("--api-version=") + flags+=("--certificate-authority=") + flags_with_completion+=("--certificate-authority") + flags_completion+=("_filedir") + flags+=("--client-certificate=") + flags_with_completion+=("--client-certificate") + flags_completion+=("_filedir") + flags+=("--client-key=") + flags_with_completion+=("--client-key") + flags_completion+=("_filedir") + flags+=("--cluster=") + flags+=("--config=") + flags_with_completion+=("--config") + flags_completion+=("_filedir") + flags+=("--context=") + flags+=("--google-json-key=") + flags+=("--insecure-skip-tls-verify") + flags+=("--log-flush-frequency=") + flags+=("--match-server-version") + flags+=("--namespace=") + two_word_flags+=("-n") + flags+=("--server=") + flags+=("--token=") + flags+=("--user=") + + must_have_one_flag=() + must_have_one_noun=() +} + _openshift_admin_ca() { last_command="openshift_admin_ca" @@ -3541,6 +3640,8 @@ _openshift_admin_ca() commands+=("create-key-pair") commands+=("create-server-cert") commands+=("create-signer-cert") + commands+=("encrypt") + commands+=("decrypt") flags=() two_word_flags=() @@ -9100,6 +9201,105 @@ _openshift_cli_adm_ca_create-signer-cert() must_have_one_noun=() } +_openshift_cli_adm_ca_encrypt() +{ + last_command="openshift_cli_adm_ca_encrypt" + commands=() + + flags=() + two_word_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--genkey=") + flags_with_completion+=("--genkey") + flags_completion+=("_filedir") + flags+=("--in=") + flags_with_completion+=("--in") + flags_completion+=("_filedir") + flags+=("--key=") + flags_with_completion+=("--key") + flags_completion+=("_filedir") + flags+=("--out=") + flags_with_completion+=("--out") + flags_completion+=("_filedir") + flags+=("--api-version=") + flags+=("--certificate-authority=") + flags_with_completion+=("--certificate-authority") + flags_completion+=("_filedir") + flags+=("--client-certificate=") + flags_with_completion+=("--client-certificate") + flags_completion+=("_filedir") + flags+=("--client-key=") + flags_with_completion+=("--client-key") + flags_completion+=("_filedir") + flags+=("--cluster=") + flags+=("--config=") + flags_with_completion+=("--config") + flags_completion+=("_filedir") + flags+=("--context=") + flags+=("--google-json-key=") + flags+=("--insecure-skip-tls-verify") + flags+=("--log-flush-frequency=") + flags+=("--match-server-version") + flags+=("--namespace=") + two_word_flags+=("-n") + flags+=("--server=") + flags+=("--token=") + flags+=("--user=") + + must_have_one_flag=() + must_have_one_noun=() +} + +_openshift_cli_adm_ca_decrypt() +{ + last_command="openshift_cli_adm_ca_decrypt" + commands=() + + flags=() + two_word_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--in=") + flags_with_completion+=("--in") + flags_completion+=("_filedir") + flags+=("--key=") + flags_with_completion+=("--key") + flags_completion+=("_filedir") + flags+=("--out=") + flags_with_completion+=("--out") + flags_completion+=("_filedir") + flags+=("--api-version=") + flags+=("--certificate-authority=") + flags_with_completion+=("--certificate-authority") + flags_completion+=("_filedir") + flags+=("--client-certificate=") + flags_with_completion+=("--client-certificate") + flags_completion+=("_filedir") + flags+=("--client-key=") + flags_with_completion+=("--client-key") + flags_completion+=("_filedir") + flags+=("--cluster=") + flags+=("--config=") + flags_with_completion+=("--config") + flags_completion+=("_filedir") + flags+=("--context=") + flags+=("--google-json-key=") + flags+=("--insecure-skip-tls-verify") + flags+=("--log-flush-frequency=") + flags+=("--match-server-version") + flags+=("--namespace=") + two_word_flags+=("-n") + flags+=("--server=") + flags+=("--token=") + flags+=("--user=") + + must_have_one_flag=() + must_have_one_noun=() +} + _openshift_cli_adm_ca() { last_command="openshift_cli_adm_ca" @@ -9108,6 +9308,8 @@ _openshift_cli_adm_ca() commands+=("create-key-pair") commands+=("create-server-cert") commands+=("create-signer-cert") + commands+=("encrypt") + commands+=("decrypt") flags=() two_word_flags=() diff --git a/docs/generated/oadm_by_example_content.adoc b/docs/generated/oadm_by_example_content.adoc index b45c941bfe3a..f57de060136d 100644 --- a/docs/generated/oadm_by_example_content.adoc +++ b/docs/generated/oadm_by_example_content.adoc @@ -23,6 +23,40 @@ Output the inputs and dependencies of your builds ==== +== oadm ca decrypt +Decrypt data encrypted with "oadm ca encrypt" + +==== + +[options="nowrap"] +---- + # Decrypt an encrypted file to a cleartext file: + $ oadm ca decrypt --key=secret.key --in=secret.encrypted --out=secret.decrypted + + # Decrypt from stdin to stdout: + $ oadm ca decrypt --key=secret.key < secret2.encrypted > secret2.decrypted + +---- +==== + + +== oadm ca encrypt +Encrypt data with AES-256-CBC encryption + +==== + +[options="nowrap"] +---- + # Encrypt the content of secret.txt with a generated key: + $ oadm ca encrypt --genkey=secret.key --in=secret.txt --out=secret.encrypted + + # Encrypt the content of secret2.txt with an existing key: + $ oadm ca encrypt --key=secret.key < secret2.txt > secret2.encrypted + +---- +==== + + == oadm config Change configuration files for the client diff --git a/docs/generated/oc_by_example_content.adoc b/docs/generated/oc_by_example_content.adoc index 22ce3c385413..f2ce0c7cf04c 100644 --- a/docs/generated/oc_by_example_content.adoc +++ b/docs/generated/oc_by_example_content.adoc @@ -23,6 +23,40 @@ Output the inputs and dependencies of your builds ==== +== oc adm ca decrypt +Decrypt data encrypted with "oc adm ca encrypt" + +==== + +[options="nowrap"] +---- + # Decrypt an encrypted file to a cleartext file: + $ oc adm ca decrypt --key=secret.key --in=secret.encrypted --out=secret.decrypted + + # Decrypt from stdin to stdout: + $ oc adm ca decrypt --key=secret.key < secret2.encrypted > secret2.decrypted + +---- +==== + + +== oc adm ca encrypt +Encrypt data with AES-256-CBC encryption + +==== + +[options="nowrap"] +---- + # Encrypt the content of secret.txt with a generated key: + $ oc adm ca encrypt --genkey=secret.key --in=secret.txt --out=secret.encrypted + + # Encrypt the content of secret2.txt with an existing key: + $ oc adm ca encrypt --key=secret.key < secret2.txt > secret2.encrypted + +---- +==== + + == oc adm config Change configuration files for the client diff --git a/hack/update-generated-swagger-descriptions.sh b/hack/update-generated-swagger-descriptions.sh index 98d342b5af04..a066c694f206 100755 --- a/hack/update-generated-swagger-descriptions.sh +++ b/hack/update-generated-swagger-descriptions.sh @@ -35,7 +35,7 @@ if [[ -z "${genswaggerdoc}" ]]; then exit 1 fi -source_files="$( find_files | grep -E '*/v1/types.go' )" +source_files="$( find_files | grep -E '/v1/types.go' )" if [[ -n "${dryrun}" ]]; then echo "The following files would be read by $0:" diff --git a/pkg/cmd/admin/admin.go b/pkg/cmd/admin/admin.go index ae573bb8f603..9a3e7396edf7 100644 --- a/pkg/cmd/admin/admin.go +++ b/pkg/cmd/admin/admin.go @@ -32,7 +32,7 @@ Administrative Commands Commands for managing a cluster are exposed here. Many administrative actions involve interaction with the command-line client as well.` -func NewCommandAdmin(name, fullName string, out io.Writer) *cobra.Command { +func NewCommandAdmin(name, fullName string, out io.Writer, errout io.Writer) *cobra.Command { // Main command cmds := &cobra.Command{ Use: name, @@ -90,7 +90,7 @@ func NewCommandAdmin(name, fullName string, out io.Writer) *cobra.Command { admin.NewCommandCreateErrorTemplate(f, admin.CreateErrorTemplateCommand, fullName+" "+admin.CreateErrorTemplateCommand, out), admin.NewCommandOverwriteBootstrapPolicy(admin.OverwriteBootstrapPolicyCommandName, fullName+" "+admin.OverwriteBootstrapPolicyCommandName, fullName+" "+admin.CreateBootstrapPolicyFileCommand, out), admin.NewCommandNodeConfig(admin.NodeConfigCommandName, fullName+" "+admin.NodeConfigCommandName, out), - cert.NewCmdCert(cert.CertRecommendedName, fullName+" "+cert.CertRecommendedName, out), + cert.NewCmdCert(cert.CertRecommendedName, fullName+" "+cert.CertRecommendedName, out, errout), }, }, } diff --git a/pkg/cmd/admin/cert/cert.go b/pkg/cmd/admin/cert/cert.go index 4bdf8b2b43d3..88cd373cc62c 100644 --- a/pkg/cmd/admin/cert/cert.go +++ b/pkg/cmd/admin/cert/cert.go @@ -12,7 +12,7 @@ import ( const CertRecommendedName = "ca" // NewCmdCert implements the OpenShift cli ca command -func NewCmdCert(name, fullName string, out io.Writer) *cobra.Command { +func NewCmdCert(name, fullName string, out io.Writer, errout io.Writer) *cobra.Command { // Parent command to which all subcommands are added. cmds := &cobra.Command{ Use: name, @@ -26,5 +26,8 @@ func NewCmdCert(name, fullName string, out io.Writer) *cobra.Command { cmds.AddCommand(admin.NewCommandCreateServerCert(admin.CreateServerCertCommandName, fullName+" "+admin.CreateServerCertCommandName, out)) cmds.AddCommand(admin.NewCommandCreateSignerCert(admin.CreateSignerCertCommandName, fullName+" "+admin.CreateSignerCertCommandName, out)) + cmds.AddCommand(admin.NewCommandEncrypt(admin.EncryptCommandName, fullName+" "+admin.EncryptCommandName, out, errout)) + cmds.AddCommand(admin.NewCommandDecrypt(admin.DecryptCommandName, fullName+" "+admin.DecryptCommandName, fullName+" "+admin.EncryptCommandName, out)) + return cmds } diff --git a/pkg/cmd/admin/groups/sync/cli/prune.go b/pkg/cmd/admin/groups/sync/cli/prune.go index d692fd4e3754..e58c7d1395bc 100644 --- a/pkg/cmd/admin/groups/sync/cli/prune.go +++ b/pkg/cmd/admin/groups/sync/cli/prune.go @@ -170,7 +170,11 @@ func (o *PruneOptions) Validate() error { // Run creates the GroupSyncer specified and runs it to sync groups // the arguments are only here because its the only way to get the printer we need func (o *PruneOptions) Run(cmd *cobra.Command, f *clientcmd.Factory) error { - clientConfig, err := ldaputil.NewLDAPClientConfig(o.Config.URL, o.Config.BindDN, o.Config.BindPassword, o.Config.CA, o.Config.Insecure) + bindPassword, err := api.ResolveStringValue(o.Config.BindPassword) + if err != nil { + return err + } + clientConfig, err := ldaputil.NewLDAPClientConfig(o.Config.URL, o.Config.BindDN, bindPassword, o.Config.CA, o.Config.Insecure) if err != nil { return fmt.Errorf("could not determine LDAP client configuration: %v", err) } diff --git a/pkg/cmd/admin/groups/sync/cli/sync.go b/pkg/cmd/admin/groups/sync/cli/sync.go index 7b3cb951253c..ed091c32f89c 100644 --- a/pkg/cmd/admin/groups/sync/cli/sync.go +++ b/pkg/cmd/admin/groups/sync/cli/sync.go @@ -337,7 +337,11 @@ func (o *SyncOptions) Validate() error { // Run creates the GroupSyncer specified and runs it to sync groups // the arguments are only here because its the only way to get the printer we need func (o *SyncOptions) Run(cmd *cobra.Command, f *clientcmd.Factory) error { - clientConfig, err := ldaputil.NewLDAPClientConfig(o.Config.URL, o.Config.BindDN, o.Config.BindPassword, o.Config.CA, o.Config.Insecure) + bindPassword, err := api.ResolveStringValue(o.Config.BindPassword) + if err != nil { + return err + } + clientConfig, err := ldaputil.NewLDAPClientConfig(o.Config.URL, o.Config.BindDN, bindPassword, o.Config.CA, o.Config.Insecure) if err != nil { return fmt.Errorf("could not determine LDAP client configuration: %v", err) } diff --git a/pkg/cmd/cli/cli.go b/pkg/cmd/cli/cli.go index b6c7a9c2c3ab..483cca02009d 100644 --- a/pkg/cmd/cli/cli.go +++ b/pkg/cmd/cli/cli.go @@ -143,7 +143,7 @@ func NewCommandCLI(name, fullName string, in io.Reader, out, errout io.Writer) * { Message: "Advanced Commands:", Commands: []*cobra.Command{ - admin.NewCommandAdmin("adm", fullName+" "+"adm", out), + admin.NewCommandAdmin("adm", fullName+" "+"adm", out, errout), cmd.NewCmdCreate(fullName, f, out), cmd.NewCmdReplace(fullName, f, out), cmd.NewCmdApply(fullName, f, out), diff --git a/pkg/cmd/openshift/openshift.go b/pkg/cmd/openshift/openshift.go index 3a4217586bc2..47db47bad6f5 100644 --- a/pkg/cmd/openshift/openshift.go +++ b/pkg/cmd/openshift/openshift.go @@ -65,7 +65,7 @@ func CommandFor(basename string) *cobra.Command { case "oc", "osc": cmd = cli.NewCommandCLI(basename, basename, in, out, errout) case "oadm", "osadm": - cmd = admin.NewCommandAdmin(basename, basename, out) + cmd = admin.NewCommandAdmin(basename, basename, out, errout) case "kubectl": cmd = cli.NewCmdKubectl(basename, out) case "kube-apiserver": @@ -107,7 +107,7 @@ func NewCommandOpenShift(name string) *cobra.Command { startAllInOne, _ := start.NewCommandStartAllInOne(name, out) root.AddCommand(startAllInOne) - root.AddCommand(admin.NewCommandAdmin("admin", name+" admin", out)) + root.AddCommand(admin.NewCommandAdmin("admin", name+" admin", out, errout)) root.AddCommand(cli.NewCommandCLI("cli", name+" cli", in, out, errout)) root.AddCommand(cli.NewCmdKubectl("kube", out)) root.AddCommand(newExperimentalCommand("ex", name+" ex")) diff --git a/pkg/cmd/server/admin/decrypt.go b/pkg/cmd/server/admin/decrypt.go new file mode 100644 index 000000000000..7dcf5909d573 --- /dev/null +++ b/pkg/cmd/server/admin/decrypt.go @@ -0,0 +1,156 @@ +package admin + +import ( + "crypto/x509" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + + "github.com/openshift/origin/pkg/cmd/util" + "github.com/spf13/cobra" + + kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + + configapi "github.com/openshift/origin/pkg/cmd/server/api" + pemutil "github.com/openshift/origin/pkg/cmd/util/pem" +) + +const DecryptCommandName = "decrypt" + +type DecryptOptions struct { + // EncryptedFile is a file containing an encrypted PEM block. + EncryptedFile string + // EncryptedData is a byte slice containing an encrypted PEM block. + EncryptedData []byte + // EncryptedReader is used to read an encrypted PEM block if no EncryptedFile or EncryptedData is provided. Cannot be a terminal reader. + EncryptedReader io.Reader + + // DecryptedFile is a destination file to write decrypted data to. + DecryptedFile string + // DecryptedWriter is used to write decrypted data to if no DecryptedFile is provided + DecryptedWriter io.Writer + + // KeyFile is a file containing a PEM block with the password to use to decrypt the data + KeyFile string +} + +const decryptExample = ` # Decrypt an encrypted file to a cleartext file: + $ %[1]s --key=secret.key --in=secret.encrypted --out=secret.decrypted + + # Decrypt from stdin to stdout: + $ %[1]s --key=secret.key < secret2.encrypted > secret2.decrypted +` + +func NewCommandDecrypt(commandName string, fullName, encryptFullName string, out io.Writer) *cobra.Command { + options := &DecryptOptions{ + EncryptedReader: os.Stdin, + DecryptedWriter: out, + } + + cmd := &cobra.Command{ + Use: commandName, + Short: fmt.Sprintf("Decrypt data encrypted with %q", encryptFullName), + Example: fmt.Sprintf(decryptExample, fullName), + Run: func(cmd *cobra.Command, args []string) { + kcmdutil.CheckErr(options.Validate(args)) + kcmdutil.CheckErr(options.Decrypt()) + }, + } + + flags := cmd.Flags() + + flags.StringVar(&options.EncryptedFile, "in", options.EncryptedFile, fmt.Sprintf("File containing encrypted data, in the format written by %q.", encryptFullName)) + flags.StringVar(&options.DecryptedFile, "out", options.DecryptedFile, "File to write the decrypted data to. Written to stdout if omitted.") + + flags.StringVar(&options.KeyFile, "key", options.KeyFile, fmt.Sprintf("The file to read the decrypting key from. Must be a PEM file in the format written by %q.", encryptFullName)) + + // autocompletion hints + cmd.MarkFlagFilename("in") + cmd.MarkFlagFilename("out") + cmd.MarkFlagFilename("key") + + return cmd +} + +func (o *DecryptOptions) Validate(args []string) error { + if len(args) != 0 { + return errors.New("no arguments are supported") + } + + if len(o.EncryptedFile) == 0 && len(o.EncryptedData) == 0 && (o.EncryptedReader == nil || util.IsTerminalReader(o.EncryptedReader)) { + return errors.New("no input data specified") + } + if len(o.EncryptedFile) > 0 && len(o.EncryptedData) > 0 { + return errors.New("cannot specify both an input file and data") + } + + if len(o.KeyFile) == 0 { + return errors.New("no key specified") + } + + return nil +} + +func (o *DecryptOptions) Decrypt() error { + // Get PEM data block + var data []byte + switch { + case len(o.EncryptedFile) > 0: + if d, err := ioutil.ReadFile(o.EncryptedFile); err != nil { + return err + } else { + data = d + } + case len(o.EncryptedData) > 0: + data = o.EncryptedData + case o.EncryptedReader != nil && !util.IsTerminalReader(o.EncryptedReader): + if d, err := ioutil.ReadAll(o.EncryptedReader); err != nil { + return err + } else { + data = d + } + } + if len(data) == 0 { + return fmt.Errorf("no input data specified") + } + dataBlock, ok := pemutil.BlockFromBytes(data, configapi.StringSourceEncryptedBlockType) + if !ok { + return fmt.Errorf("input does not contain a valid PEM block of type %q", configapi.StringSourceEncryptedBlockType) + } + + // Get password + keyBlock, ok, err := pemutil.BlockFromFile(o.KeyFile, configapi.StringSourceKeyBlockType) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("%s does not contain a valid PEM block of type %q", o.KeyFile, configapi.StringSourceKeyBlockType) + } + if len(keyBlock.Bytes) == 0 { + return fmt.Errorf("%s does not contain a key", o.KeyFile) + } + password := keyBlock.Bytes + + // Decrypt + plaintext, err := x509.DecryptPEMBlock(dataBlock, password) + if err != nil { + return err + } + + // Write decrypted data + switch { + case len(o.DecryptedFile) > 0: + if err := ioutil.WriteFile(o.DecryptedFile, plaintext, os.FileMode(0600)); err != nil { + return err + } + case o.DecryptedWriter != nil: + fmt.Fprint(o.DecryptedWriter, string(plaintext)) + if util.IsTerminalWriter(o.DecryptedWriter) { + fmt.Fprintln(o.DecryptedWriter) + } + } + + return nil +} diff --git a/pkg/cmd/server/admin/encrypt.go b/pkg/cmd/server/admin/encrypt.go new file mode 100644 index 000000000000..b04745d261dd --- /dev/null +++ b/pkg/cmd/server/admin/encrypt.go @@ -0,0 +1,206 @@ +package admin + +import ( + "crypto/rand" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "unicode" + "unicode/utf8" + + "github.com/spf13/cobra" + + kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + + configapi "github.com/openshift/origin/pkg/cmd/server/api" + "github.com/openshift/origin/pkg/cmd/util" + pemutil "github.com/openshift/origin/pkg/cmd/util/pem" +) + +const EncryptCommandName = "encrypt" + +type EncryptOptions struct { + // CleartextFile contains cleartext data to encrypt. + CleartextFile string + // CleartextData is cleartext data to encrypt. + CleartextData []byte + // CleartextReader reads cleartext data to encrypt if CleartextReader and CleartextFile are unspecified. + CleartextReader io.Reader + + // EncryptedFile has encrypted data written to it. + EncryptedFile string + // EncryptedWriter has encrypted data written to it if EncryptedFile is unspecified. + EncryptedWriter io.Writer + + // KeyFile contains the password in PEM format (as previously written by GenKeyFile) + KeyFile string + // GenKeyFile indicates a key should be generated and written + GenKeyFile string + + // PromptWriter is used to write status and prompt messages + PromptWriter io.Writer +} + +const encryptExample = ` # Encrypt the content of secret.txt with a generated key: + $ %[1]s --genkey=secret.key --in=secret.txt --out=secret.encrypted + + # Encrypt the content of secret2.txt with an existing key: + $ %[1]s --key=secret.key < secret2.txt > secret2.encrypted +` + +func NewCommandEncrypt(commandName string, fullName string, out io.Writer, errout io.Writer) *cobra.Command { + options := &EncryptOptions{ + CleartextReader: os.Stdin, + EncryptedWriter: out, + PromptWriter: errout, + } + + cmd := &cobra.Command{ + Use: commandName, + Short: "Encrypt data with AES-256-CBC encryption", + Example: fmt.Sprintf(encryptExample, fullName), + Run: func(cmd *cobra.Command, args []string) { + kcmdutil.CheckErr(options.Validate(args)) + kcmdutil.CheckErr(options.Encrypt()) + }, + } + + flags := cmd.Flags() + + flags.StringVar(&options.CleartextFile, "in", options.CleartextFile, "File containing the data to encrypt. Read from stdin if omitted.") + flags.StringVar(&options.EncryptedFile, "out", options.EncryptedFile, "File to write the encrypted data to. Written to stdout if omitted.") + + flags.StringVar(&options.KeyFile, "key", options.KeyFile, "File containing the encrypting key from in the format written by --genkey.") + flags.StringVar(&options.GenKeyFile, "genkey", options.GenKeyFile, "File to write a randomly generated key to.") + + // autocompletion hints + cmd.MarkFlagFilename("in") + cmd.MarkFlagFilename("out") + cmd.MarkFlagFilename("key") + cmd.MarkFlagFilename("genkey") + + return cmd +} + +func (o *EncryptOptions) Validate(args []string) error { + if len(args) != 0 { + return errors.New("no arguments are supported") + } + + if len(o.CleartextFile) == 0 && len(o.CleartextData) == 0 && o.CleartextReader == nil { + return errors.New("an input file, data, or reader is required") + } + if len(o.CleartextFile) > 0 && len(o.CleartextData) > 0 { + return errors.New("cannot specify both an input file and data") + } + + if len(o.EncryptedFile) == 0 && o.EncryptedWriter == nil { + return errors.New("an output file or writer is required") + } + + if len(o.GenKeyFile) > 0 && len(o.KeyFile) > 0 { + return errors.New("either --genkey or --key may be specified, not both") + } + if len(o.GenKeyFile) == 0 && len(o.KeyFile) == 0 { + return errors.New("--genkey or --key is required") + } + + return nil +} + +func (o *EncryptOptions) Encrypt() error { + // Get data + var data []byte + var warnWhitespace = true + switch { + case len(o.CleartextFile) > 0: + if d, err := ioutil.ReadFile(o.CleartextFile); err != nil { + return err + } else { + data = d + } + case len(o.CleartextData) > 0: + // Don't warn in cases where we're explicitly being given the data to use + warnWhitespace = false + data = o.CleartextData + case o.CleartextReader != nil && util.IsTerminalReader(o.CleartextReader) && o.PromptWriter != nil: + // Read a single line from stdin with prompting + data = []byte(util.PromptForString(o.CleartextReader, o.PromptWriter, "Data to encrypt: ")) + case o.CleartextReader != nil: + // Read data from stdin without prompting (allows binary data and piping) + if d, err := ioutil.ReadAll(o.CleartextReader); err != nil { + return err + } else { + data = d + } + } + if warnWhitespace && (o.PromptWriter != nil) && (len(data) > 0) { + r1, _ := utf8.DecodeRune(data) + r2, _ := utf8.DecodeLastRune(data) + if unicode.IsSpace(r1) || unicode.IsSpace(r2) { + fmt.Fprintln(o.PromptWriter, "Warning: Data includes leading or trailing whitespace, which will be included in the encrypted value") + } + } + + // Get key + var key []byte + switch { + case len(o.KeyFile) > 0: + if block, ok, err := pemutil.BlockFromFile(o.KeyFile, configapi.StringSourceKeyBlockType); err != nil { + return err + } else if !ok { + return fmt.Errorf("%s does not contain a valid PEM block of type %q", o.KeyFile, configapi.StringSourceKeyBlockType) + } else if len(block.Bytes) == 0 { + return fmt.Errorf("%s does not contain a key", o.KeyFile) + } else { + key = block.Bytes + } + case len(o.GenKeyFile) > 0: + key = make([]byte, 32) + if _, err := rand.Read(key); err != nil { + return err + } + } + if len(key) == 0 { + return errors.New("--genkey or --key is required") + } + + // Encrypt + dataBlock, err := x509.EncryptPEMBlock(rand.Reader, configapi.StringSourceEncryptedBlockType, data, key, x509.PEMCipherAES256) + if err != nil { + return err + } + + // Write data + if len(o.EncryptedFile) > 0 { + if err := pemutil.BlockToFile(o.EncryptedFile, dataBlock, os.FileMode(0644)); err != nil { + return err + } + } else if o.EncryptedWriter != nil { + encryptedBytes, err := pemutil.BlockToBytes(dataBlock) + if err != nil { + return err + } + n, err := o.EncryptedWriter.Write(encryptedBytes) + if err != nil { + return err + } + if n != len(encryptedBytes) { + return fmt.Errorf("could not completely write encrypted data") + } + } + + // Write key + if len(o.GenKeyFile) > 0 { + keyBlock := &pem.Block{Bytes: key, Type: configapi.StringSourceKeyBlockType} + if err := pemutil.BlockToFile(o.GenKeyFile, keyBlock, os.FileMode(0600)); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/cmd/server/api/helpers.go b/pkg/cmd/server/api/helpers.go index a3f6718a9d22..25d39bb618ff 100644 --- a/pkg/cmd/server/api/helpers.go +++ b/pkg/cmd/server/api/helpers.go @@ -176,6 +176,7 @@ func GetMasterFileReferences(config *MasterConfig) []*string { case (*LDAPPasswordIdentityProvider): refs = append(refs, &provider.CA) + refs = append(refs, GetStringSourceFileReferences(&provider.BindPassword)...) case (*BasicAuthPasswordIdentityProvider): refs = append(refs, &provider.RemoteConnectionInfo.CA) @@ -189,9 +190,17 @@ func GetMasterFileReferences(config *MasterConfig) []*string { case (*GitLabIdentityProvider): refs = append(refs, &provider.CA) + refs = append(refs, GetStringSourceFileReferences(&provider.ClientSecret)...) case (*OpenIDIdentityProvider): refs = append(refs, &provider.CA) + refs = append(refs, GetStringSourceFileReferences(&provider.ClientSecret)...) + + case (*GoogleIdentityProvider): + refs = append(refs, GetStringSourceFileReferences(&provider.ClientSecret)...) + + case (*GitHubIdentityProvider): + refs = append(refs, GetStringSourceFileReferences(&provider.ClientSecret)...) } } diff --git a/pkg/cmd/server/api/serialization_test.go b/pkg/cmd/server/api/serialization_test.go index d210234d27f2..b388e9a9db5f 100644 --- a/pkg/cmd/server/api/serialization_test.go +++ b/pkg/cmd/server/api/serialization_test.go @@ -173,6 +173,13 @@ func fuzzInternalObject(t *testing.T, forVersion unversioned.GroupVersion, item obj.MappingMethod = "claim" } }, + func(s *configapi.StringSource, c fuzz.Continue) { + if c.RandBool() { + c.Fuzz(&s.Value) + } else { + c.Fuzz(&s.StringSourceSpec) + } + }, ) f.Fuzz(item) diff --git a/pkg/cmd/server/api/stringsource.go b/pkg/cmd/server/api/stringsource.go new file mode 100644 index 000000000000..c6c47eb697af --- /dev/null +++ b/pkg/cmd/server/api/stringsource.go @@ -0,0 +1,61 @@ +package api + +import ( + "crypto/x509" + "fmt" + "io/ioutil" + "os" + + pemutil "github.com/openshift/origin/pkg/cmd/util/pem" +) + +func GetStringSourceFileReferences(s *StringSource) []*string { + if s == nil { + return nil + } + return []*string{ + &s.File, + &s.KeyFile, + } +} + +func ResolveStringValue(s StringSource) (string, error) { + var value string + switch { + case len(s.Value) > 0: + value = s.Value + case len(s.Env) > 0: + value = os.Getenv(s.Env) + case len(s.File) > 0: + data, err := ioutil.ReadFile(s.File) + if err != nil { + return "", err + } + value = string(data) + default: + value = "" + } + + if len(s.KeyFile) == 0 { + // value is cleartext, return + return value, nil + } + + keyData, err := ioutil.ReadFile(s.KeyFile) + if err != nil { + return "", err + } + + secretBlock, ok := pemutil.BlockFromBytes([]byte(value), StringSourceEncryptedBlockType) + if !ok { + return "", fmt.Errorf("no valid PEM block of type %q found in data", StringSourceEncryptedBlockType) + } + + keyBlock, ok := pemutil.BlockFromBytes(keyData, StringSourceKeyBlockType) + if !ok { + return "", fmt.Errorf("no valid PEM block of type %q found in key", StringSourceKeyBlockType) + } + + data, err := x509.DecryptPEMBlock(secretBlock, keyBlock.Bytes) + return string(data), err +} diff --git a/pkg/cmd/server/api/stringsource_test.go b/pkg/cmd/server/api/stringsource_test.go new file mode 100644 index 000000000000..2a208e78e9dd --- /dev/null +++ b/pkg/cmd/server/api/stringsource_test.go @@ -0,0 +1,182 @@ +package api + +import ( + "io/ioutil" + "os" + "strings" + "testing" +) + +var ( + // encrypted data generated by `oadm ca encrypt` + // if the output of `oadm ca encrypt` changes, add additional tests, + // but do not modify this, to ensure we continue to decrypt these values + encryptedData = []byte(`-----BEGIN ENCRYPTED STRING----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,8db8d0db237a56c8ea8d3c5c21acc5b6 + +9JUj8ezx/3MDgiiWbnx9QA== +-----END ENCRYPTED STRING-----`) + + // encrypting key generated by `oadm ca encrypt` + // if the output of `oadm ca encrypt` changes, add additional tests, + // but do not modify this, to ensure we continue to decrypt these values + encryptingKey = []byte(`-----BEGIN ENCRYPTING KEY----- +f3zQfReuhwI1BvBNglhZdjgSocKKqABwyGafHJcdORw= +-----END ENCRYPTING KEY-----`) +) + +func TestStringSource(t *testing.T) { + os.Setenv("TestStringSource_present_env", "envvalue") + os.Setenv("TestStringSource_encrypted_env", string(encryptedData)) + + emptyFile, err := ioutil.TempFile("", "empty_file") + if err != nil { + t.Fatal(err) + } + defer os.Remove(emptyFile.Name()) // clean up + + fooFile, err := ioutil.TempFile("", "foo_file") + if err != nil { + t.Fatal(err) + } + defer os.Remove(emptyFile.Name()) // clean up + if err := ioutil.WriteFile(fooFile.Name(), []byte(`filevalue`), os.FileMode(0755)); err != nil { + t.Fatal(err) + } + + encryptedFile, err := ioutil.TempFile("", "encrypted_file") + if err != nil { + t.Fatal(err) + } + defer os.Remove(encryptedFile.Name()) // clean up + if err := ioutil.WriteFile(encryptedFile.Name(), encryptedData, os.FileMode(0755)); err != nil { + t.Fatal(err) + } + + validKeyFile, err := ioutil.TempFile("", "valid_key_file") + if err != nil { + t.Fatal(err) + } + defer os.Remove(validKeyFile.Name()) // clean up + if err := ioutil.WriteFile(validKeyFile.Name(), encryptingKey, os.FileMode(0755)); err != nil { + t.Fatal(err) + } + + invalidKeyFile, err := ioutil.TempFile("", "invalid_key_file") + if err != nil { + t.Fatal(err) + } + defer os.Remove(invalidKeyFile.Name()) // clean up + if err := ioutil.WriteFile(invalidKeyFile.Name(), []byte(`invalid key value`), os.FileMode(0600)); err != nil { + t.Fatal(err) + } + + testcases := map[string]struct { + StringSource StringSource + ExpectedValue string + ExpectedError string + }{ + "empty": { + StringSource: StringSource{}, + ExpectedValue: "", + ExpectedError: "", + }, + "value": { + StringSource: StringSource{StringSourceSpec: StringSourceSpec{Value: "foo"}}, + ExpectedValue: "foo", + ExpectedError: "", + }, + "env empty": { + StringSource: StringSource{StringSourceSpec: StringSourceSpec{Env: "empty_env"}}, + ExpectedValue: "", + ExpectedError: "", + }, + "env present": { + StringSource: StringSource{StringSourceSpec: StringSourceSpec{Env: "TestStringSource_present_env"}}, + ExpectedValue: "envvalue", + ExpectedError: "", + }, + "file missing": { + StringSource: StringSource{StringSourceSpec: StringSourceSpec{File: "missing_file"}}, + ExpectedValue: "", + ExpectedError: "missing_file: no such file", + }, + "file empty": { + StringSource: StringSource{StringSourceSpec: StringSourceSpec{File: emptyFile.Name()}}, + ExpectedValue: "", + ExpectedError: "", + }, + "file present": { + StringSource: StringSource{StringSourceSpec: StringSourceSpec{File: fooFile.Name()}}, + ExpectedValue: "filevalue", + ExpectedError: "", + }, + + "encrypted env with missing key": { + StringSource: StringSource{StringSourceSpec: StringSourceSpec{Env: "TestStringSource_encrypted_env", KeyFile: "missing_key"}}, + ExpectedValue: "", + ExpectedError: "missing_key: no such file", + }, + "encrypted env with invalid key": { + StringSource: StringSource{StringSourceSpec: StringSourceSpec{Env: "TestStringSource_encrypted_env", KeyFile: invalidKeyFile.Name()}}, + ExpectedValue: "", + ExpectedError: "no valid PEM block", + }, + "encrypted env with valid key": { + StringSource: StringSource{StringSourceSpec: StringSourceSpec{Env: "TestStringSource_encrypted_env", KeyFile: validKeyFile.Name()}}, + ExpectedValue: "encryptedvalue", + ExpectedError: "", + }, + + "encrypted value with missing key": { + StringSource: StringSource{StringSourceSpec: StringSourceSpec{Value: string(encryptedData), KeyFile: "missing_key"}}, + ExpectedValue: "", + ExpectedError: "missing_key: no such file", + }, + "encrypted value with invalid key": { + StringSource: StringSource{StringSourceSpec: StringSourceSpec{Value: string(encryptedData), KeyFile: invalidKeyFile.Name()}}, + ExpectedValue: "", + ExpectedError: "no valid PEM block", + }, + "encrypted value with valid key": { + StringSource: StringSource{StringSourceSpec: StringSourceSpec{Value: string(encryptedData), KeyFile: validKeyFile.Name()}}, + ExpectedValue: "encryptedvalue", + ExpectedError: "", + }, + + "missing encrypted file with valid key": { + StringSource: StringSource{StringSourceSpec: StringSourceSpec{File: "missing_file", KeyFile: validKeyFile.Name()}}, + ExpectedValue: "", + ExpectedError: "missing_file: no such file", + }, + "encrypted file with missing key": { + StringSource: StringSource{StringSourceSpec: StringSourceSpec{File: encryptedFile.Name(), KeyFile: "missing_key"}}, + ExpectedValue: "", + ExpectedError: "missing_key: no such file", + }, + "encrypted file with invalid key": { + StringSource: StringSource{StringSourceSpec: StringSourceSpec{File: encryptedFile.Name(), KeyFile: invalidKeyFile.Name()}}, + ExpectedValue: "", + ExpectedError: "no valid PEM block", + }, + "encrypted file with valid key": { + StringSource: StringSource{StringSourceSpec: StringSourceSpec{File: encryptedFile.Name(), KeyFile: validKeyFile.Name()}}, + ExpectedValue: "encryptedvalue", + ExpectedError: "", + }, + } + for k, tc := range testcases { + value, err := ResolveStringValue(tc.StringSource) + + if len(tc.ExpectedError) > 0 && (err == nil || !strings.Contains(err.Error(), tc.ExpectedError)) { + t.Errorf("%s: expected error containing %q, got %q", k, tc.ExpectedError, err.Error()) + } + if len(tc.ExpectedError) == 0 && err != nil { + t.Errorf("%s: got unexpected error: %v", k, err) + } + if tc.ExpectedValue != value { + t.Errorf("%s: Expected value=%q, got %q", k, tc.ExpectedValue, value) + } + } +} diff --git a/pkg/cmd/server/api/types.go b/pkg/cmd/server/api/types.go index c4c80aa5f9cd..1b28c5382e85 100644 --- a/pkg/cmd/server/api/types.go +++ b/pkg/cmd/server/api/types.go @@ -616,7 +616,8 @@ type LDAPPasswordIdentityProvider struct { // BindDN is an optional DN to bind with during the search phase. BindDN string // BindPassword is an optional password to bind with during the search phase. - BindPassword string + BindPassword StringSource + // Insecure, if true, indicates the connection should not use TLS. // Cannot be set to true with a URL scheme of "ldaps://" // If false, "ldaps://" URLs connect using TLS, and "ldap://" URLs are upgraded to a TLS connection using StartTLS as specified in https://tools.ietf.org/html/rfc2830 @@ -689,7 +690,7 @@ type GitHubIdentityProvider struct { // ClientID is the oauth client ID ClientID string // ClientSecret is the oauth client secret - ClientSecret string + ClientSecret StringSource // Organizations optionally restricts which organizations are allowed to log in Organizations []string } @@ -705,7 +706,7 @@ type GitLabIdentityProvider struct { // ClientID is the oauth client ID ClientID string // ClientSecret is the oauth client secret - ClientSecret string + ClientSecret StringSource } type GoogleIdentityProvider struct { @@ -714,7 +715,7 @@ type GoogleIdentityProvider struct { // ClientID is the oauth client ID ClientID string // ClientSecret is the oauth client secret - ClientSecret string + ClientSecret StringSource // HostedDomain is the optional Google App domain (e.g. "mycompany.com") to restrict logins to HostedDomain string @@ -730,7 +731,7 @@ type OpenIDIdentityProvider struct { // ClientID is the oauth client ID ClientID string // ClientSecret is the oauth client secret - ClientSecret string + ClientSecret StringSource // ExtraScopes are any scopes to request in addition to the standard "openid" scope. ExtraScopes []string @@ -870,6 +871,35 @@ type AssetExtensionsConfig struct { HTML5Mode bool } +const ( + // StringSourceEncryptedBlockType is the PEM block type used to store an encrypted string + StringSourceEncryptedBlockType = "ENCRYPTED STRING" + // StringSourceKeyBlockType is the PEM block type used to store an encrypting key + StringSourceKeyBlockType = "ENCRYPTING KEY" +) + +// StringSource allows specifying a string inline, or externally via env var or file. +// When it contains only a string value, it marshals to a simple JSON string. +type StringSource struct { + // StringSourceSpec specifies the string value, or external location + StringSourceSpec +} + +// StringSourceSpec specifies a string value, or external location +type StringSourceSpec struct { + // Value specifies the cleartext value, or an encrypted value if keyFile is specified. + Value string + + // Env specifies an envvar containing the cleartext value, or an encrypted value if the keyFile is specified. + Env string + + // File references a file containing the cleartext value, or an encrypted value if a keyFile is specified. + File string + + // KeyFile references a file containing the key to use to decrypt the value. + KeyFile string +} + type LDAPSyncConfig struct { unversioned.TypeMeta @@ -878,7 +908,8 @@ type LDAPSyncConfig struct { // BindDN is an optional DN to bind with during the search phase. BindDN string // BindPassword is an optional password to bind with during the search phase. - BindPassword string + BindPassword StringSource + // Insecure, if true, indicates the connection should not use TLS. // Cannot be set to true with a URL scheme of "ldaps://" // If false, "ldaps://" URLs connect using TLS, and "ldap://" URLs are upgraded to a TLS connection using StartTLS as specified in https://tools.ietf.org/html/rfc2830 diff --git a/pkg/cmd/server/api/v1/stringsource.go b/pkg/cmd/server/api/v1/stringsource.go new file mode 100644 index 000000000000..89b0bb5b532f --- /dev/null +++ b/pkg/cmd/server/api/v1/stringsource.go @@ -0,0 +1,31 @@ +package v1 + +import "encoding/json" + +// UnmarshalJSON implements the json.Unmarshaller interface. +// If the value is a string, it sets the Value field of the StringSource. +// Otherwise, it is unmarshaled into the StringSourceSpec struct +func (s *StringSource) UnmarshalJSON(value []byte) error { + // If we can unmarshal to a simple string, just set the value + var simpleValue string + if err := json.Unmarshal(value, &simpleValue); err == nil { + s.Value = simpleValue + return nil + } + + // Otherwise do the full struct unmarshal + return json.Unmarshal(value, &s.StringSourceSpec) +} + +// MarshalJSON implements the json.Marshaller interface. +// If the StringSource contains only a string Value (or is empty), it is marshaled as a JSON string. +// Otherwise, the StringSourceSpec struct is marshaled as a JSON object. +func (s StringSource) MarshalJSON() ([]byte, error) { + // If we have only a cleartext value set, do a simple string marshal + if s.StringSourceSpec == (StringSourceSpec{Value: s.Value}) { + return json.Marshal(s.Value) + } + + // Otherwise do the full struct marshal of the externalized bits + return json.Marshal(s.StringSourceSpec) +} diff --git a/pkg/cmd/server/api/v1/stringsource_test.go b/pkg/cmd/server/api/v1/stringsource_test.go new file mode 100644 index 000000000000..d84f73d815cd --- /dev/null +++ b/pkg/cmd/server/api/v1/stringsource_test.go @@ -0,0 +1,144 @@ +package v1 + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/runtime/serializer" + + configapi "github.com/openshift/origin/pkg/cmd/server/api" +) + +func TestStringSourceUnmarshaling(t *testing.T) { + codec := serializer.NewCodecFactory(configapi.Scheme).LegacyCodec(SchemeGroupVersion) + + testcases := map[string]struct { + JSON string + ExpectedObject configapi.StringSource + ExpectedError string + }{ + "bool": { + JSON: `true`, + ExpectedObject: configapi.StringSource{}, + ExpectedError: "cannot unmarshal", + }, + "number": { + JSON: `1`, + ExpectedObject: configapi.StringSource{}, + ExpectedError: "cannot unmarshal", + }, + + "empty string": { + JSON: `""`, + ExpectedObject: configapi.StringSource{}, + ExpectedError: "", + }, + "string": { + JSON: `"foo"`, + ExpectedObject: configapi.StringSource{StringSourceSpec: configapi.StringSourceSpec{Value: "foo"}}, + ExpectedError: "", + }, + + "empty struct": { + JSON: `{}`, + ExpectedObject: configapi.StringSource{}, + ExpectedError: "", + }, + "struct value": { + JSON: `{"value":"foo"}`, + ExpectedObject: configapi.StringSource{StringSourceSpec: configapi.StringSourceSpec{Value: "foo"}}, + ExpectedError: "", + }, + "struct env": { + JSON: `{"env":"foo"}`, + ExpectedObject: configapi.StringSource{StringSourceSpec: configapi.StringSourceSpec{Env: "foo"}}, + ExpectedError: "", + }, + "struct file": { + JSON: `{"file":"foo"}`, + ExpectedObject: configapi.StringSource{StringSourceSpec: configapi.StringSourceSpec{File: "foo"}}, + ExpectedError: "", + }, + "struct file+keyFile": { + JSON: `{"file":"foo","keyFile":"bar"}`, + ExpectedObject: configapi.StringSource{StringSourceSpec: configapi.StringSourceSpec{File: "foo", KeyFile: "bar"}}, + ExpectedError: "", + }, + } + + for k, tc := range testcases { + // Wrap in a dummy object we can deserialize + input := fmt.Sprintf(`{"kind":"GitHubIdentityProvider","apiVersion":"v1","clientSecret":%s}`, tc.JSON) + obj, err := runtime.Decode(codec, []byte(input)) + if len(tc.ExpectedError) > 0 && (err == nil || !strings.Contains(err.Error(), tc.ExpectedError)) { + t.Errorf("%s: expected error containing %q, got %q", k, tc.ExpectedError, err.Error()) + } + if len(tc.ExpectedError) == 0 && err != nil { + t.Errorf("%s: got unexpected error: %v", k, err) + } + if err != nil { + continue + } + githubProvider, ok := obj.(*configapi.GitHubIdentityProvider) + if !ok { + t.Errorf("%s: wrapper object was not a GitHubIdentityProvider as expected: %#v", k, obj) + continue + } + if !reflect.DeepEqual(tc.ExpectedObject, githubProvider.ClientSecret) { + t.Errorf("%s: expected\n%#v\ngot\n%#v", k, tc.ExpectedObject, githubProvider.ClientSecret) + } + } +} + +func TestStringSourceMarshaling(t *testing.T) { + codec := serializer.NewCodecFactory(configapi.Scheme).LegacyCodec(SchemeGroupVersion) + + testcases := map[string]struct { + Object configapi.StringSource + ExpectedJSON string + }{ + "empty string": { + Object: configapi.StringSource{}, + ExpectedJSON: `""`, + }, + "string": { + Object: configapi.StringSource{StringSourceSpec: configapi.StringSourceSpec{Value: "foo"}}, + ExpectedJSON: `"foo"`, + }, + "struct value+keyFile": { + Object: configapi.StringSource{StringSourceSpec: configapi.StringSourceSpec{Value: "foo", KeyFile: "bar"}}, + ExpectedJSON: `{"value":"foo","env":"","file":"","keyFile":"bar"}`, + }, + "struct env": { + Object: configapi.StringSource{StringSourceSpec: configapi.StringSourceSpec{Env: "foo"}}, + ExpectedJSON: `{"value":"","env":"foo","file":"","keyFile":""}`, + }, + "struct file": { + Object: configapi.StringSource{StringSourceSpec: configapi.StringSourceSpec{File: "foo"}}, + ExpectedJSON: `{"value":"","env":"","file":"foo","keyFile":""}`, + }, + "struct file+keyFile": { + Object: configapi.StringSource{StringSourceSpec: configapi.StringSourceSpec{File: "foo", KeyFile: "bar"}}, + ExpectedJSON: `{"value":"","env":"","file":"foo","keyFile":"bar"}`, + }, + } + + for k, tc := range testcases { + provider := &configapi.GitHubIdentityProvider{ClientSecret: tc.Object} + + json, err := runtime.Encode(codec, provider) + if err != nil { + t.Errorf("%s: unexpected error: %v", k, err) + } + + // Wrap in a dummy JSON from the surrounding object + input := fmt.Sprintf(`{"kind":"GitHubIdentityProvider","apiVersion":"v1","clientID":"","clientSecret":%s,"organizations":null}`, tc.ExpectedJSON) + if strings.TrimSpace(string(json)) != input { + t.Log(len(input), len(json)) + t.Errorf("%s: expected\n%s\ngot\n%s", k, input, string(json)) + } + } +} diff --git a/pkg/cmd/server/api/v1/swagger_doc.go b/pkg/cmd/server/api/v1/swagger_doc.go index e7dfdbafa838..0ec8c021dd2f 100644 --- a/pkg/cmd/server/api/v1/swagger_doc.go +++ b/pkg/cmd/server/api/v1/swagger_doc.go @@ -690,6 +690,26 @@ func (SessionSecrets) SwaggerDoc() map[string]string { return map_SessionSecrets } +var map_StringSource = map[string]string{ + "": "StringSource allows specifying a string inline, or externally via env var or file. When it contains only a string value, it marshals to a simple JSON string.", +} + +func (StringSource) SwaggerDoc() map[string]string { + return map_StringSource +} + +var map_StringSourceSpec = map[string]string{ + "": "StringSourceSpec specifies a string value, or external location", + "value": "Value specifies the cleartext value, or an encrypted value if keyFile is specified.", + "env": "Env specifies an envvar containing the cleartext value, or an encrypted value if the keyFile is specified.", + "file": "File references a file containing the cleartext value, or an encrypted value if a keyFile is specified.", + "keyFile": "KeyFile references a file containing the key to use to decrypt the value.", +} + +func (StringSourceSpec) SwaggerDoc() map[string]string { + return map_StringSourceSpec +} + var map_TokenConfig = map[string]string{ "": "TokenConfig holds the necessary configuration options for authorization and access tokens", "authorizeTokenMaxAgeSeconds": "AuthorizeTokenMaxAgeSeconds defines the maximum age of authorize tokens", diff --git a/pkg/cmd/server/api/v1/types.go b/pkg/cmd/server/api/v1/types.go index c3e2d0ff9143..8998718d4964 100644 --- a/pkg/cmd/server/api/v1/types.go +++ b/pkg/cmd/server/api/v1/types.go @@ -607,7 +607,8 @@ type LDAPPasswordIdentityProvider struct { // BindDN is an optional DN to bind with during the search phase. BindDN string `json:"bindDN"` // BindPassword is an optional password to bind with during the search phase. - BindPassword string `json:"bindPassword"` + BindPassword StringSource `json:"bindPassword"` + // Insecure, if true, indicates the connection should not use TLS. // Cannot be set to true with a URL scheme of "ldaps://" // If false, "ldaps://" URLs connect using TLS, and "ldap://" URLs are upgraded to a TLS connection using StartTLS as specified in https://tools.ietf.org/html/rfc2830 @@ -684,7 +685,7 @@ type GitHubIdentityProvider struct { // ClientID is the oauth client ID ClientID string `json:"clientID"` // ClientSecret is the oauth client secret - ClientSecret string `json:"clientSecret"` + ClientSecret StringSource `json:"clientSecret"` // Organizations optionally restricts which organizations are allowed to log in Organizations []string `json:"organizations"` } @@ -701,7 +702,7 @@ type GitLabIdentityProvider struct { // ClientID is the oauth client ID ClientID string `json:"clientID"` // ClientSecret is the oauth client secret - ClientSecret string `json:"clientSecret"` + ClientSecret StringSource `json:"clientSecret"` } // GoogleIdentityProvider provides identities for users authenticating using Google credentials @@ -711,7 +712,7 @@ type GoogleIdentityProvider struct { // ClientID is the oauth client ID ClientID string `json:"clientID"` // ClientSecret is the oauth client secret - ClientSecret string `json:"clientSecret"` + ClientSecret StringSource `json:"clientSecret"` // HostedDomain is the optional Google App domain (e.g. "mycompany.com") to restrict logins to HostedDomain string `json:"hostedDomain"` @@ -728,7 +729,7 @@ type OpenIDIdentityProvider struct { // ClientID is the oauth client ID ClientID string `json:"clientID"` // ClientSecret is the oauth client secret - ClientSecret string `json:"clientSecret"` + ClientSecret StringSource `json:"clientSecret"` // ExtraScopes are any scopes to request in addition to the standard "openid" scope. ExtraScopes []string `json:"extraScopes"` @@ -876,6 +877,28 @@ type AssetExtensionsConfig struct { HTML5Mode bool `json:"html5Mode"` } +// StringSource allows specifying a string inline, or externally via env var or file. +// When it contains only a string value, it marshals to a simple JSON string. +type StringSource struct { + // StringSourceSpec specifies the string value, or external location + StringSourceSpec `json:",inline"` +} + +// StringSourceSpec specifies a string value, or external location +type StringSourceSpec struct { + // Value specifies the cleartext value, or an encrypted value if keyFile is specified. + Value string `json:"value"` + + // Env specifies an envvar containing the cleartext value, or an encrypted value if the keyFile is specified. + Env string `json:"env"` + + // File references a file containing the cleartext value, or an encrypted value if a keyFile is specified. + File string `json:"file"` + + // KeyFile references a file containing the key to use to decrypt the value. + KeyFile string `json:"keyFile"` +} + // LDAPSyncConfig holds the necessary configuration options to define an LDAP group sync type LDAPSyncConfig struct { unversioned.TypeMeta `json:",inline"` @@ -885,7 +908,8 @@ type LDAPSyncConfig struct { // BindDN is an optional DN to bind to the LDAP server with BindDN string `json:"bindDN"` // BindPassword is an optional password to bind with during the search phase. - BindPassword string `json:"bindPassword"` + BindPassword StringSource `json:"bindPassword"` + // Insecure, if true, indicates the connection should not use TLS. // Cannot be set to true with a URL scheme of "ldaps://" // If false, "ldaps://" URLs connect using TLS, and "ldap://" URLs are upgraded to a TLS connection using StartTLS as specified in https://tools.ietf.org/html/rfc2830 diff --git a/pkg/cmd/server/api/v1/types_test.go b/pkg/cmd/server/api/v1/types_test.go index 0378e4c063b5..ebef7645f751 100644 --- a/pkg/cmd/server/api/v1/types_test.go +++ b/pkg/cmd/server/api/v1/types_test.go @@ -237,6 +237,27 @@ oauthConfig: insecure: false kind: LDAPPasswordIdentityProvider url: "" + - challenge: false + login: false + mappingMethod: "" + name: "" + provider: + apiVersion: v1 + attributes: + email: null + id: null + name: null + preferredUsername: null + bindDN: "" + bindPassword: + env: "" + file: filename + keyFile: "" + value: "" + ca: "" + insecure: false + kind: LDAPPasswordIdentityProvider + url: "" - challenge: false login: false mappingMethod: "" @@ -273,6 +294,20 @@ oauthConfig: clientSecret: "" kind: GitHubIdentityProvider organizations: null + - challenge: false + login: false + mappingMethod: "" + name: "" + provider: + apiVersion: v1 + clientID: "" + clientSecret: + env: "" + file: filename + keyFile: "" + value: "" + kind: GitHubIdentityProvider + organizations: null - challenge: false login: false mappingMethod: "" @@ -284,6 +319,21 @@ oauthConfig: clientSecret: "" kind: GitLabIdentityProvider url: "" + - challenge: false + login: false + mappingMethod: "" + name: "" + provider: + apiVersion: v1 + ca: "" + clientID: "" + clientSecret: + env: "" + file: filename + keyFile: "" + value: "" + kind: GitLabIdentityProvider + url: "" - challenge: false login: false mappingMethod: "" @@ -294,6 +344,20 @@ oauthConfig: clientSecret: "" hostedDomain: "" kind: GoogleIdentityProvider + - challenge: false + login: false + mappingMethod: "" + name: "" + provider: + apiVersion: v1 + clientID: "" + clientSecret: + env: "" + file: filename + keyFile: "" + value: "" + hostedDomain: "" + kind: GoogleIdentityProvider - challenge: false login: false mappingMethod: "" @@ -315,6 +379,31 @@ oauthConfig: authorize: "" token: "" userInfo: "" + - challenge: false + login: false + mappingMethod: "" + name: "" + provider: + apiVersion: v1 + ca: "" + claims: + email: null + id: null + name: null + preferredUsername: null + clientID: "" + clientSecret: + env: "" + file: filename + keyFile: "" + value: "" + extraAuthorizeParameters: null + extraScopes: null + kind: OpenIDIdentityProvider + urls: + authorize: "" + token: "" + userInfo: "" masterCA: null masterPublicURL: "" masterURL: "" @@ -409,12 +498,17 @@ func TestMasterConfig(t *testing.T) { {Provider: &internal.DenyAllPasswordIdentityProvider{}}, {Provider: &internal.HTPasswdPasswordIdentityProvider{}}, {Provider: &internal.LDAPPasswordIdentityProvider{}}, + {Provider: &internal.LDAPPasswordIdentityProvider{BindPassword: internal.StringSource{StringSourceSpec: internal.StringSourceSpec{File: "filename"}}}}, {Provider: &internal.RequestHeaderIdentityProvider{}}, {Provider: &internal.KeystonePasswordIdentityProvider{}}, {Provider: &internal.GitHubIdentityProvider{}}, + {Provider: &internal.GitHubIdentityProvider{ClientSecret: internal.StringSource{StringSourceSpec: internal.StringSourceSpec{File: "filename"}}}}, {Provider: &internal.GitLabIdentityProvider{}}, + {Provider: &internal.GitLabIdentityProvider{ClientSecret: internal.StringSource{StringSourceSpec: internal.StringSourceSpec{File: "filename"}}}}, {Provider: &internal.GoogleIdentityProvider{}}, + {Provider: &internal.GoogleIdentityProvider{ClientSecret: internal.StringSource{StringSourceSpec: internal.StringSourceSpec{File: "filename"}}}}, {Provider: &internal.OpenIDIdentityProvider{}}, + {Provider: &internal.OpenIDIdentityProvider{ClientSecret: internal.StringSource{StringSourceSpec: internal.StringSourceSpec{File: "filename"}}}}, }, SessionConfig: &internal.SessionConfig{}, Templates: &internal.OAuthTemplates{}, diff --git a/pkg/cmd/server/api/validation/ldap.go b/pkg/cmd/server/api/validation/ldap.go index 4ff0295a7c70..12fb20b6c08b 100644 --- a/pkg/cmd/server/api/validation/ldap.go +++ b/pkg/cmd/server/api/validation/ldap.go @@ -12,7 +12,11 @@ import ( ) func ValidateLDAPSyncConfig(config *api.LDAPSyncConfig) ValidationResults { - validationResults := ValidateLDAPClientConfig(config.URL, config.BindDN, config.BindPassword, config.CA, config.Insecure, nil) + validationResults := ValidationResults{} + + validationResults.Append(ValidateStringSource(config.BindPassword, field.NewPath("bindPassword"))) + bindPassword, _ := api.ResolveStringValue(config.BindPassword) + validationResults.Append(ValidateLDAPClientConfig(config.URL, config.BindDN, bindPassword, config.CA, config.Insecure, nil)) schemaConfigsFound := []string{} @@ -65,7 +69,7 @@ func ValidateLDAPClientConfig(url, bindDN, bindPassword, CA string, insecure boo if (len(bindDN) == 0) != (len(bindPassword) == 0) { validationResults.AddErrors(field.Invalid(fldPath.Child("bindDN"), bindDN, "bindDN and bindPassword must both be specified, or both be empty")) - validationResults.AddErrors(field.Invalid(fldPath.Child("bindPassword"), "", + validationResults.AddErrors(field.Invalid(fldPath.Child("bindPassword"), "(masked)", "bindDN and bindPassword must both be specified, or both be empty")) } diff --git a/pkg/cmd/server/api/validation/oauth.go b/pkg/cmd/server/api/validation/oauth.go index b731ad0cb554..74a74acfa51a 100644 --- a/pkg/cmd/server/api/validation/oauth.go +++ b/pkg/cmd/server/api/validation/oauth.go @@ -168,7 +168,7 @@ func ValidateIdentityProvider(identityProvider api.IdentityProvider, fldPath *fi } else { switch provider := identityProvider.Provider.(type) { case (*api.RequestHeaderIdentityProvider): - validationResults.Append(ValidateRequestHeaderIdentityProvider(provider, identityProvider)) + validationResults.Append(ValidateRequestHeaderIdentityProvider(provider, identityProvider, fldPath)) case (*api.BasicAuthPasswordIdentityProvider): validationResults.AddErrors(ValidateRemoteConnectionInfo(provider.RemoteConnectionInfo, providerPath)...) @@ -177,22 +177,22 @@ func ValidateIdentityProvider(identityProvider api.IdentityProvider, fldPath *fi validationResults.AddErrors(ValidateFile(provider.File, providerPath.Child("file"))...) case (*api.LDAPPasswordIdentityProvider): - validationResults.Append(ValidateLDAPIdentityProvider(provider)) + validationResults.Append(ValidateLDAPIdentityProvider(provider, providerPath)) case (*api.KeystonePasswordIdentityProvider): validationResults.Append(ValidateKeystoneIdentityProvider(provider, identityProvider, providerPath)) case (*api.GitHubIdentityProvider): - validationResults.AddErrors(ValidateOAuthIdentityProvider(provider.ClientID, provider.ClientSecret, identityProvider.UseAsChallenger)...) + validationResults.AddErrors(ValidateOAuthIdentityProvider(provider.ClientID, provider.ClientSecret, identityProvider.UseAsChallenger, fldPath)...) case (*api.GitLabIdentityProvider): - validationResults.AddErrors(ValidateGitLabIdentityProvider(provider, identityProvider.UseAsChallenger)...) + validationResults.AddErrors(ValidateGitLabIdentityProvider(provider, identityProvider.UseAsChallenger, fldPath)...) case (*api.GoogleIdentityProvider): - validationResults.AddErrors(ValidateOAuthIdentityProvider(provider.ClientID, provider.ClientSecret, identityProvider.UseAsChallenger)...) + validationResults.AddErrors(ValidateOAuthIdentityProvider(provider.ClientID, provider.ClientSecret, identityProvider.UseAsChallenger, fldPath)...) case (*api.OpenIDIdentityProvider): - validationResults.AddErrors(ValidateOpenIDIdentityProvider(provider, identityProvider)...) + validationResults.AddErrors(ValidateOpenIDIdentityProvider(provider, identityProvider, fldPath)...) } } @@ -200,13 +200,16 @@ func ValidateIdentityProvider(identityProvider api.IdentityProvider, fldPath *fi return validationResults } -func ValidateLDAPIdentityProvider(provider *api.LDAPPasswordIdentityProvider) ValidationResults { - providerPath := field.NewPath("provider") - validationResults := ValidateLDAPClientConfig(provider.URL, provider.BindDN, provider.BindPassword, provider.CA, provider.Insecure, providerPath) +func ValidateLDAPIdentityProvider(provider *api.LDAPPasswordIdentityProvider, fldPath *field.Path) ValidationResults { + validationResults := ValidationResults{} + + validationResults.Append(ValidateStringSource(provider.BindPassword, fldPath.Child("bindPassword"))) + bindPassword, _ := api.ResolveStringValue(provider.BindPassword) + validationResults.Append(ValidateLDAPClientConfig(provider.URL, provider.BindDN, bindPassword, provider.CA, provider.Insecure, fldPath)) // At least one attribute to use as the user id is required if len(provider.Attributes.ID) == 0 { - validationResults.AddErrors(field.Invalid(providerPath.Child("attributes", "id"), "[]", "at least one id attribute is required (LDAP standard identity attribute is 'dn')")) + validationResults.AddErrors(field.Invalid(fldPath.Child("attributes", "id"), "[]", "at least one id attribute is required (LDAP standard identity attribute is 'dn')")) } return validationResults @@ -230,28 +233,28 @@ func ValidateKeystoneIdentityProvider(provider *api.KeystonePasswordIdentityProv return validationResults } -func ValidateRequestHeaderIdentityProvider(provider *api.RequestHeaderIdentityProvider, identityProvider api.IdentityProvider) ValidationResults { +func ValidateRequestHeaderIdentityProvider(provider *api.RequestHeaderIdentityProvider, identityProvider api.IdentityProvider, fieldPath *field.Path) ValidationResults { validationResults := ValidationResults{} if len(provider.ClientCA) > 0 { - validationResults.AddErrors(ValidateFile(provider.ClientCA, field.NewPath("provider", "clientCA"))...) + validationResults.AddErrors(ValidateFile(provider.ClientCA, fieldPath.Child("provider", "clientCA"))...) } if len(provider.Headers) == 0 { - validationResults.AddErrors(field.Required(field.NewPath("provider", "headers"), "")) + validationResults.AddErrors(field.Required(fieldPath.Child("provider", "headers"), "")) } if identityProvider.UseAsChallenger && len(provider.ChallengeURL) == 0 { - err := field.Required(field.NewPath("provider", "challengeURL"), "") + err := field.Required(fieldPath.Child("provider", "challengeURL"), "") err.Detail = "challengeURL is required if challenge=true" validationResults.AddErrors(err) } if identityProvider.UseAsLogin && len(provider.LoginURL) == 0 { - err := field.Required(field.NewPath("provider", "loginURL"), "") + err := field.Required(fieldPath.Child("provider", "loginURL"), "") err.Detail = "loginURL is required if login=true" validationResults.AddErrors(err) } if len(provider.ChallengeURL) > 0 { - url, urlErrs := ValidateURL(provider.ChallengeURL, field.NewPath("provider", "challengeURL")) + url, urlErrs := ValidateURL(provider.ChallengeURL, fieldPath.Child("provider", "challengeURL")) validationResults.AddErrors(urlErrs...) if len(urlErrs) == 0 && !strings.Contains(url.RawQuery, redirector.URLToken) && !strings.Contains(url.RawQuery, redirector.QueryToken) { validationResults.AddWarnings( @@ -264,12 +267,12 @@ func ValidateRequestHeaderIdentityProvider(provider *api.RequestHeaderIdentityPr } } if len(provider.LoginURL) > 0 { - url, urlErrs := ValidateURL(provider.LoginURL, field.NewPath("provider", "loginURL")) + url, urlErrs := ValidateURL(provider.LoginURL, fieldPath.Child("provider", "loginURL")) validationResults.AddErrors(urlErrs...) if len(urlErrs) == 0 && !strings.Contains(url.RawQuery, redirector.URLToken) && !strings.Contains(url.RawQuery, redirector.QueryToken) { validationResults.AddWarnings( field.Invalid( - field.NewPath("provider", "loginURL"), + fieldPath.Child("provider", "loginURL"), provider.LoginURL, fmt.Sprintf("query does not include %q or %q, redirect will not preserve original authorize parameters", redirector.URLToken, redirector.QueryToken), ), @@ -279,52 +282,58 @@ func ValidateRequestHeaderIdentityProvider(provider *api.RequestHeaderIdentityPr // Warn if it looks like they expect direct requests to the OAuth endpoints, and have not secured the header checking with a client certificate check if len(provider.ClientCA) == 0 && (len(provider.ChallengeURL) > 0 || len(provider.LoginURL) > 0) { - validationResults.AddWarnings(field.Invalid(field.NewPath("provider", "clientCA"), "", "if no clientCA is set, no request verification is done, and any request directly against the OAuth server can impersonate any identity from this provider")) + validationResults.AddWarnings(field.Invalid(fieldPath.Child("provider", "clientCA"), "", "if no clientCA is set, no request verification is done, and any request directly against the OAuth server can impersonate any identity from this provider")) } return validationResults } -func ValidateOAuthIdentityProvider(clientID, clientSecret string, challenge bool) field.ErrorList { +func ValidateOAuthIdentityProvider(clientID string, clientSecret api.StringSource, challenge bool, fieldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if len(clientID) == 0 { - allErrs = append(allErrs, field.Required(field.NewPath("provider", "clientID"), "")) - } - if len(clientSecret) == 0 { - allErrs = append(allErrs, field.Required(field.NewPath("provider", "clientSecret"), "")) + allErrs = append(allErrs, field.Required(fieldPath.Child("provider", "clientID"), "")) + } + clientSecretResults := ValidateStringSource(clientSecret, fieldPath.Child("provider", "clientSecret")) + allErrs = append(allErrs, clientSecretResults.Errors...) + if len(clientSecretResults.Errors) == 0 { + clientSecret, err := api.ResolveStringValue(clientSecret) + if err != nil { + allErrs = append(allErrs, field.Invalid(fieldPath.Child("provider", "clientSecret"), "", err.Error())) + } else if len(clientSecret) == 0 { + allErrs = append(allErrs, field.Required(fieldPath.Child("provider", "clientSecret"), "")) + } } if challenge { - allErrs = append(allErrs, field.Invalid(field.NewPath("challenge"), challenge, "oauth providers cannot be used for challenges")) + allErrs = append(allErrs, field.Invalid(fieldPath.Child("challenge"), challenge, "OAuth/OpenID providers cannot be used for challenges")) } return allErrs } -func ValidateGitLabIdentityProvider(provider *api.GitLabIdentityProvider, challenge bool) field.ErrorList { +func ValidateGitLabIdentityProvider(provider *api.GitLabIdentityProvider, challenge bool, fieldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} - allErrs = append(allErrs, ValidateOAuthIdentityProvider(provider.ClientID, provider.ClientSecret, challenge)...) + allErrs = append(allErrs, ValidateOAuthIdentityProvider(provider.ClientID, provider.ClientSecret, challenge, fieldPath)...) - providerPath := field.NewPath("provider") - _, urlErrs := ValidateSecureURL(provider.URL, providerPath.Child("url")) + _, urlErrs := ValidateSecureURL(provider.URL, fieldPath.Child("provider", "url")) allErrs = append(allErrs, urlErrs...) if len(provider.CA) != 0 { - allErrs = append(allErrs, ValidateFile(provider.CA, providerPath.Child("ca"))...) + allErrs = append(allErrs, ValidateFile(provider.CA, fieldPath.Child("provider", "ca"))...) } return allErrs } -func ValidateOpenIDIdentityProvider(provider *api.OpenIDIdentityProvider, identityProvider api.IdentityProvider) field.ErrorList { +func ValidateOpenIDIdentityProvider(provider *api.OpenIDIdentityProvider, identityProvider api.IdentityProvider, fieldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} - allErrs = append(allErrs, ValidateOAuthIdentityProvider(provider.ClientID, provider.ClientSecret, identityProvider.UseAsChallenger)...) + allErrs = append(allErrs, ValidateOAuthIdentityProvider(provider.ClientID, provider.ClientSecret, identityProvider.UseAsChallenger, fieldPath)...) // Communication with the Authorization Endpoint MUST utilize TLS // http://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint - providerPath := field.NewPath("provider") + providerPath := fieldPath.Child("provider") urlsPath := providerPath.Child("urls") _, urlErrs := ValidateSecureURL(provider.URLs.Authorize, urlsPath.Child("authorize")) allErrs = append(allErrs, urlErrs...) @@ -357,7 +366,7 @@ func validateGrantConfig(config api.GrantConfig, fldPath *field.Path) field.Erro allErrs := field.ErrorList{} if !api.ValidGrantHandlerTypes.Has(string(config.Method)) { - allErrs = append(allErrs, field.Invalid(field.NewPath("grantConfig", "method"), config.Method, fmt.Sprintf("must be one of: %v", api.ValidGrantHandlerTypes.List()))) + allErrs = append(allErrs, field.Invalid(fldPath.Child("method"), config.Method, fmt.Sprintf("must be one of: %v", api.ValidGrantHandlerTypes.List()))) } return allErrs @@ -367,7 +376,7 @@ func validateSessionConfig(config *api.SessionConfig, fldPath *field.Path) field allErrs := field.ErrorList{} // Validate session secrets file, if specified - sessionSecretsFilePath := field.NewPath("sessionSecretsFile") + sessionSecretsFilePath := fldPath.Child("sessionSecretsFile") if len(config.SessionSecretsFile) > 0 { fileErrs := ValidateFile(config.SessionSecretsFile, sessionSecretsFilePath) if len(fileErrs) != 0 { @@ -387,7 +396,7 @@ func validateSessionConfig(config *api.SessionConfig, fldPath *field.Path) field } if len(config.SessionName) == 0 { - allErrs = append(allErrs, field.Required(field.NewPath("sessionName"), "")) + allErrs = append(allErrs, field.Required(fldPath.Child("sessionName"), "")) } return allErrs diff --git a/pkg/cmd/server/api/validation/validation.go b/pkg/cmd/server/api/validation/validation.go index 2809e87c9e10..fdc9de9be474 100644 --- a/pkg/cmd/server/api/validation/validation.go +++ b/pkg/cmd/server/api/validation/validation.go @@ -4,10 +4,13 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "io/ioutil" "net" "net/url" "os" "strings" + "unicode" + "unicode/utf8" "github.com/spf13/pflag" @@ -21,6 +24,43 @@ import ( cmdflags "github.com/openshift/origin/pkg/cmd/util/flags" ) +func ValidateStringSource(s api.StringSource, fieldPath *field.Path) ValidationResults { + validationResults := ValidationResults{} + methods := 0 + if len(s.Value) > 0 { + methods++ + } + if len(s.File) > 0 { + methods++ + fileErrors := ValidateFile(s.File, fieldPath.Child("file")) + validationResults.AddErrors(fileErrors...) + + // If the file was otherwise ok, and its value will be used verbatim, warn about trailing whitespace + if len(fileErrors) == 0 && len(s.KeyFile) == 0 { + if data, err := ioutil.ReadFile(s.File); err != nil { + validationResults.AddErrors(field.Invalid(fieldPath.Child("file"), s.File, fmt.Sprintf("could not read file: %v", err))) + } else if len(data) > 0 { + r, _ := utf8.DecodeLastRune(data) + if unicode.IsSpace(r) { + validationResults.AddWarnings(field.Invalid(fieldPath.Child("file"), s.File, "contains trailing whitespace which will be included in the value")) + } + } + } + } + if len(s.Env) > 0 { + methods++ + } + if methods > 1 { + validationResults.AddErrors(field.Invalid(fieldPath, "", "only one of value, file, and env can be specified")) + } + + if len(s.KeyFile) > 0 { + validationResults.AddErrors(ValidateFile(s.KeyFile, fieldPath.Child("keyFile"))...) + } + + return validationResults +} + func ValidateHostPort(value string, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} diff --git a/pkg/cmd/server/origin/auth.go b/pkg/cmd/server/origin/auth.go index bab4ddb2985a..595e1744a3fc 100644 --- a/pkg/cmd/server/origin/auth.go +++ b/pkg/cmd/server/origin/auth.go @@ -465,23 +465,39 @@ func (c *AuthConfig) getAuthenticationHandler(mux cmdutil.Mux, errorHandler hand func (c *AuthConfig) getOAuthProvider(identityProvider configapi.IdentityProvider) (external.Provider, error) { switch provider := identityProvider.Provider.(type) { case (*configapi.GitHubIdentityProvider): - return github.NewProvider(identityProvider.Name, provider.ClientID, provider.ClientSecret, provider.Organizations), nil + clientSecret, err := configapi.ResolveStringValue(provider.ClientSecret) + if err != nil { + return nil, err + } + return github.NewProvider(identityProvider.Name, provider.ClientID, clientSecret, provider.Organizations), nil case (*configapi.GitLabIdentityProvider): transport, err := cmdutil.TransportFor(provider.CA, "", "") if err != nil { return nil, err } - return gitlab.NewProvider(identityProvider.Name, transport, provider.URL, provider.ClientID, provider.ClientSecret) + clientSecret, err := configapi.ResolveStringValue(provider.ClientSecret) + if err != nil { + return nil, err + } + return gitlab.NewProvider(identityProvider.Name, transport, provider.URL, provider.ClientID, clientSecret) case (*configapi.GoogleIdentityProvider): - return google.NewProvider(identityProvider.Name, provider.ClientID, provider.ClientSecret, provider.HostedDomain) + clientSecret, err := configapi.ResolveStringValue(provider.ClientSecret) + if err != nil { + return nil, err + } + return google.NewProvider(identityProvider.Name, provider.ClientID, clientSecret, provider.HostedDomain) case (*configapi.OpenIDIdentityProvider): transport, err := cmdutil.TransportFor(provider.CA, "", "") if err != nil { return nil, err } + clientSecret, err := configapi.ResolveStringValue(provider.ClientSecret) + if err != nil { + return nil, err + } // OpenID Connect requests MUST contain the openid scope value // http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest @@ -490,7 +506,7 @@ func (c *AuthConfig) getOAuthProvider(identityProvider configapi.IdentityProvide config := openid.Config{ ClientID: provider.ClientID, - ClientSecret: provider.ClientSecret, + ClientSecret: clientSecret, Scopes: scopes.List(), @@ -533,9 +549,13 @@ func (c *AuthConfig) getPasswordAuthenticator(identityProvider configapi.Identit return nil, fmt.Errorf("Error parsing LDAPPasswordIdentityProvider URL: %v", err) } + bindPassword, err := configapi.ResolveStringValue(provider.BindPassword) + if err != nil { + return nil, err + } clientConfig, err := ldaputil.NewLDAPClientConfig(provider.URL, provider.BindDN, - provider.BindPassword, + bindPassword, provider.CA, provider.Insecure) if err != nil { diff --git a/pkg/cmd/util/pem/pem.go b/pkg/cmd/util/pem/pem.go new file mode 100644 index 000000000000..3e736d6f4729 --- /dev/null +++ b/pkg/cmd/util/pem/pem.go @@ -0,0 +1,50 @@ +package pem + +import ( + "bytes" + "encoding/pem" + "io/ioutil" + "os" + "path/filepath" +) + +func BlockFromFile(path string, blockType string) (*pem.Block, bool, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, false, err + } + block, ok := BlockFromBytes(data, blockType) + return block, ok, nil +} + +func BlockFromBytes(data []byte, blockType string) (*pem.Block, bool) { + for { + block, remaining := pem.Decode(data) + if block == nil { + return nil, false + } + if block.Type == blockType { + return block, true + } + data = remaining + } +} + +func BlockToFile(path string, block *pem.Block, mode os.FileMode) error { + b, err := BlockToBytes(block) + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(path), os.FileMode(0755)); err != nil { + return err + } + return ioutil.WriteFile(path, b, mode) +} + +func BlockToBytes(block *pem.Block) ([]byte, error) { + b := bytes.Buffer{} + if err := pem.Encode(&b, block); err != nil { + return nil, err + } + return b.Bytes(), nil +} diff --git a/pkg/cmd/util/terminal.go b/pkg/cmd/util/terminal.go index 6fb6d4cd5952..a69464ad10d6 100644 --- a/pkg/cmd/util/terminal.go +++ b/pkg/cmd/util/terminal.go @@ -109,6 +109,12 @@ func readInputFromReader(r io.Reader) string { return result } +// IsTerminalReader returns whether the passed io.Reader is a terminal or not +func IsTerminalReader(r io.Reader) bool { + file, ok := r.(*os.File) + return ok && term.IsTerminal(file.Fd()) +} + // IsTerminalWriter returns whether the passed io.Writer is a terminal or not func IsTerminalWriter(w io.Writer) bool { file, ok := w.(*os.File) diff --git a/test/cmd/admin.sh b/test/cmd/admin.sh index 4923696d000c..b2bdd329ba26 100755 --- a/test/cmd/admin.sh +++ b/test/cmd/admin.sh @@ -71,6 +71,29 @@ os::cmd::expect_failure_and_text 'oadm ca create-master-certs --hostnames=exampl os::cmd::expect_failure_and_text 'oadm ca create-master-certs --hostnames=example.com --master=example.com' 'master must be a valid URL' os::cmd::expect_failure_and_text 'oadm ca create-master-certs --hostnames=example.com --master=https://example.com --public-master=example.com' 'public master must be a valid URL' +# check encrypt/decrypt of plain text +os::cmd::expect_success 'echo -n "secret data 1" | oadm ca encrypt --genkey=secret.key --out=secret.encrypted' +os::cmd::expect_success_and_text 'oadm ca decrypt --in=secret.encrypted --key=secret.key' '^secret data 1$' +# create a file with trailing whitespace +echo "data with newline" > secret.whitespace.data +os::cmd::expect_success_and_text 'oadm ca encrypt --key=secret.key --in=secret.whitespace.data --out=secret.whitespace.encrypted' 'Warning.*whitespace' +os::cmd::expect_success 'oadm ca decrypt --key=secret.key --in=secret.whitespace.encrypted --out=secret.whitespace.decrypted' +os::cmd::expect_success 'diff secret.whitespace.data secret.whitespace.decrypted' +# create a binary file +echo "hello" | gzip > secret.data +# encrypt using file and pipe input/output +os::cmd::expect_success 'oadm ca encrypt --key=secret.key --in=secret.data --out=secret.file-in-file-out.encrypted' +os::cmd::expect_success 'oadm ca encrypt --key=secret.key --in=secret.data > secret.file-in-pipe-out.encrypted' +os::cmd::expect_success 'oadm ca encrypt --key=secret.key < secret.data > secret.pipe-in-pipe-out.encrypted' +# decrypt using all three methods +os::cmd::expect_success 'oadm ca decrypt --key=secret.key --in=secret.file-in-file-out.encrypted --out=secret.file-in-file-out.decrypted' +os::cmd::expect_success 'oadm ca decrypt --key=secret.key --in=secret.file-in-pipe-out.encrypted > secret.file-in-pipe-out.decrypted' +os::cmd::expect_success 'oadm ca decrypt --key=secret.key < secret.pipe-in-pipe-out.encrypted > secret.pipe-in-pipe-out.decrypted' +# verify lossless roundtrip +os::cmd::expect_success 'diff secret.data secret.file-in-file-out.decrypted' +os::cmd::expect_success 'diff secret.data secret.file-in-pipe-out.decrypted' +os::cmd::expect_success 'diff secret.data secret.pipe-in-pipe-out.decrypted' + os::cmd::expect_success 'oc create -f examples/hello-openshift/hello-pod.json' # os::cmd::expect_success_and_text 'oadm manage-node --list-pods' 'hello-openshift' # os::cmd::expect_success_and_text 'oadm manage-node --list-pods' '(unassigned|assigned)' diff --git a/test/integration/oauth_ldap_test.go b/test/integration/oauth_ldap_test.go index 899a3f5cb387..df2d88284e45 100644 --- a/test/integration/oauth_ldap_test.go +++ b/test/integration/oauth_ldap_test.go @@ -3,7 +3,10 @@ package integration import ( + "bytes" "fmt" + "io/ioutil" + "os" "reflect" "testing" @@ -13,7 +16,9 @@ import ( authapi "github.com/openshift/origin/pkg/auth/api" "github.com/openshift/origin/pkg/client" + "github.com/openshift/origin/pkg/cmd/server/admin" configapi "github.com/openshift/origin/pkg/cmd/server/api" + configapilatest "github.com/openshift/origin/pkg/cmd/server/api/latest" "github.com/openshift/origin/pkg/cmd/util/tokencmd" testutil "github.com/openshift/origin/test/util" testserver "github.com/openshift/origin/test/util/server" @@ -82,17 +87,42 @@ func TestOAuthLDAP(t *testing.T) { t.Fatalf("unexpected error: %v", err) } + // Generate an encrypted file/keyfile to contain the bindPassword + bindPasswordFile, err := ioutil.TempFile("", "bindPassword") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer os.Remove(bindPasswordFile.Name()) + bindPasswordKeyFile, err := ioutil.TempFile("", "bindPasswordKey") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer os.Remove(bindPasswordKeyFile.Name()) + encryptOpts := &admin.EncryptOptions{ + CleartextData: []byte(bindPassword), + EncryptedFile: bindPasswordFile.Name(), + GenKeyFile: bindPasswordKeyFile.Name(), + } + if err := encryptOpts.Encrypt(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + masterOptions.OAuthConfig.IdentityProviders[0] = configapi.IdentityProvider{ Name: providerName, UseAsChallenger: true, UseAsLogin: true, MappingMethod: "claim", Provider: &configapi.LDAPPasswordIdentityProvider{ - URL: fmt.Sprintf("ldap://%s/%s?%s?%s?%s", ldapAddress, searchDN, searchAttr, searchScope, searchFilter), - BindDN: bindDN, - BindPassword: bindPassword, - Insecure: true, - CA: "", + URL: fmt.Sprintf("ldap://%s/%s?%s?%s?%s", ldapAddress, searchDN, searchAttr, searchScope, searchFilter), + BindDN: bindDN, + BindPassword: configapi.StringSource{ + configapi.StringSourceSpec{ + File: bindPasswordFile.Name(), + KeyFile: bindPasswordKeyFile.Name(), + }, + }, + Insecure: true, + CA: "", Attributes: configapi.LDAPAttributeMapping{ ID: []string{idAttr1, idAttr2}, PreferredUsername: []string{loginAttr1, loginAttr2}, @@ -102,6 +132,23 @@ func TestOAuthLDAP(t *testing.T) { }, } + // serialize to YAML to make sure a complex StringSource survives a round-trip + serializedOptions, err := configapilatest.WriteYAML(masterOptions) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // read back in + deserializedObject, err := configapilatest.ReadYAML(bytes.NewBuffer(serializedOptions)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // assert type and proceed, using the deserialized version as our config + if deserializedOptions, ok := deserializedObject.(*configapi.MasterConfig); !ok { + t.Fatalf("unexpected object: %v", deserializedObject) + } else { + masterOptions = deserializedOptions + } + clusterAdminKubeConfig, err := testserver.StartConfiguredMaster(masterOptions) if err != nil { t.Fatalf("unexpected error: %v", err) diff --git a/tools/genbashcomp/gen_openshift_bash_comp.go b/tools/genbashcomp/gen_openshift_bash_comp.go index e4d691b9441f..5c6374c5d195 100644 --- a/tools/genbashcomp/gen_openshift_bash_comp.go +++ b/tools/genbashcomp/gen_openshift_bash_comp.go @@ -62,6 +62,6 @@ func main() { oc.GenBashCompletionFile(outFile_osc) outFile_osadm := outDir + "oadm" - oadm := admin.NewCommandAdmin("oadm", "openshift admin", ioutil.Discard) + oadm := admin.NewCommandAdmin("oadm", "openshift admin", ioutil.Discard, ioutil.Discard) oadm.GenBashCompletionFile(outFile_osadm) } diff --git a/tools/gendocs/gen_openshift_docs.go b/tools/gendocs/gen_openshift_docs.go index 091d597fbd1e..71ca8a539979 100644 --- a/tools/gendocs/gen_openshift_docs.go +++ b/tools/gendocs/gen_openshift_docs.go @@ -51,6 +51,6 @@ func main() { gendocs.GenDocs(cmd, outFile) outFile = outDir + "oadm_by_example_content.adoc" - cmd = admin.NewCommandAdmin("oadm", "oadm", ioutil.Discard) + cmd = admin.NewCommandAdmin("oadm", "oadm", ioutil.Discard, ioutil.Discard) gendocs.GenDocs(cmd, outFile) }