From d93aa7467828be68873be663878f674cb8eee838 Mon Sep 17 00:00:00 2001 From: lucdew Date: Wed, 12 Jun 2024 15:28:57 +0200 Subject: [PATCH 1/2] Add mTLS support --- docs/index.md | 2 ++ keycloak/keycloak_client.go | 18 +++++++++++++----- keycloak/keycloak_client_test.go | 5 +++-- provider/provider.go | 16 +++++++++++++++- provider/provider_test.go | 25 ++++++++++++++----------- 5 files changed, 47 insertions(+), 19 deletions(-) diff --git a/docs/index.md b/docs/index.md index d85db9e6c..01069ba3a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -86,6 +86,8 @@ The following arguments are supported: - `initial_login` - (Optional) Optionally avoid Keycloak login during provider setup, for when Keycloak itself is being provisioned by terraform. Defaults to true, which is the original method. - `client_timeout` - (Optional) Sets the timeout of the client when addressing Keycloak, in seconds. Defaults to the environment variable `KEYCLOAK_CLIENT_TIMEOUT`, or `5` if the environment variable is not specified. - `tls_insecure_skip_verify` - (Optional) Allows ignoring insecure certificates when set to `true`. Defaults to `false`. Disabling this security check is dangerous and should only be done in local or test environments. +- `tls_client_certificate` - (Optional) The TLS client certificate in PEM format when the keycloak server is configured with TLS mutual authentication. +- `tls_client_private_key` - (Optional) The TLS client pkcs1 private key in PEM format when the keycloak server is configured with TLS mutual authentication. - `root_ca_certificate` - (Optional) Allows x509 calls using an unknown CA certificate (for development purposes) - `base_path` - (Optional) The base path used for accessing the Keycloak REST API. Defaults to the environment variable `KEYCLOAK_BASE_PATH`, or an empty string if the environment variable is not specified. Note that users of the legacy distribution of Keycloak will need to set this attribute to `/auth`. - `additional_headers` - (Optional) A map of custom HTTP headers to add to each request to the Keycloak API. diff --git a/keycloak/keycloak_client.go b/keycloak/keycloak_client.go index 23e896128..97ba30ab0 100644 --- a/keycloak/keycloak_client.go +++ b/keycloak/keycloak_client.go @@ -7,7 +7,6 @@ import ( "crypto/x509" "encoding/json" "fmt" - "github.com/hashicorp/terraform-plugin-log/tflog" "io/ioutil" "net/http" "net/http/cookiejar" @@ -17,6 +16,8 @@ import ( "strings" "time" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/go-version" "golang.org/x/net/publicsuffix" @@ -60,7 +61,7 @@ var redHatSSO7VersionMap = map[int]string{ 4: "9.0.17", } -func NewKeycloakClient(ctx context.Context, url, basePath, clientId, clientSecret, realm, username, password string, initialLogin bool, clientTimeout int, caCert string, tlsInsecureSkipVerify bool, userAgent string, redHatSSO bool, additionalHeaders map[string]string) (*KeycloakClient, error) { +func NewKeycloakClient(ctx context.Context, url, basePath, clientId, clientSecret, realm, username, password string, initialLogin bool, clientTimeout int, caCert string, tlsInsecureSkipVerify bool, userAgent string, redHatSSO bool, additionalHeaders map[string]string, tlsClientCert string, tlsClientPrivateKey string) (*KeycloakClient, error) { clientCredentials := &ClientCredentials{ ClientId: clientId, ClientSecret: clientSecret, @@ -79,7 +80,7 @@ func NewKeycloakClient(ctx context.Context, url, basePath, clientId, clientSecre } } - httpClient, err := newHttpClient(tlsInsecureSkipVerify, clientTimeout, caCert) + httpClient, err := newHttpClient(tlsInsecureSkipVerify, clientTimeout, caCert, tlsClientCert, tlsClientPrivateKey) if err != nil { return nil, fmt.Errorf("failed to create http client: %v", err) } @@ -170,7 +171,6 @@ func (keycloakClient *KeycloakClient) login(ctx context.Context) error { serverVersion = strings.ReplaceAll(info.SystemInfo.ServerVersion, ".GA", "") } else { regex, err := regexp.Compile(`\.redhat-\w+`) - if err != nil { fmt.Println("Error compiling regex:", err) return err @@ -499,7 +499,7 @@ func (keycloakClient *KeycloakClient) marshal(body interface{}) ([]byte, error) return json.Marshal(body) } -func newHttpClient(tlsInsecureSkipVerify bool, clientTimeout int, caCert string) (*http.Client, error) { +func newHttpClient(tlsInsecureSkipVerify bool, clientTimeout int, caCert string, tlsClientCert string, tlsClientPrivateKey string) (*http.Client, error) { cookieJar, err := cookiejar.New(&cookiejar.Options{ PublicSuffixList: publicsuffix.List, }) @@ -518,6 +518,14 @@ func newHttpClient(tlsInsecureSkipVerify bool, clientTimeout int, caCert string) transport.TLSClientConfig.RootCAs = caCertPool } + if tlsClientCert != "" && tlsClientPrivateKey != "" { + clientKeyPairCert, err := tls.X509KeyPair([]byte(tlsClientCert), []byte(tlsClientPrivateKey)) + if err != nil { + return nil, err + } + transport.TLSClientConfig.Certificates = []tls.Certificate{clientKeyPairCert} + } + retryClient := retryablehttp.NewClient() retryClient.RetryMax = 1 retryClient.RetryWaitMin = time.Second * 1 diff --git a/keycloak/keycloak_client_test.go b/keycloak/keycloak_client_test.go index 6493c215b..d828a2110 100644 --- a/keycloak/keycloak_client_test.go +++ b/keycloak/keycloak_client_test.go @@ -2,10 +2,11 @@ package keycloak import ( "context" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "os" "strconv" "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" ) var requiredEnvironmentVariables = []string{ @@ -50,7 +51,7 @@ func TestAccKeycloakApiClientRefresh(t *testing.T) { keycloakClient, err := NewKeycloakClient(ctx, os.Getenv("KEYCLOAK_URL"), "", os.Getenv("KEYCLOAK_CLIENT_ID"), os.Getenv("KEYCLOAK_CLIENT_SECRET"), os.Getenv("KEYCLOAK_REALM"), os.Getenv("KEYCLOAK_USER"), os.Getenv("KEYCLOAK_PASSWORD"), true, clientTimeout, "", false, "", false, map[string]string{ "foo": "bar", - }) + }, "", "") if err != nil { t.Fatalf("%s", err) } diff --git a/provider/provider.go b/provider/provider.go index 00e08e889..147b17a35 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -174,6 +174,18 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider { Description: "Allows ignoring insecure certificates when set to true. Defaults to false. Disabling security check is dangerous and should be avoided.", Default: false, }, + "tls_client_certificate": { + Optional: true, + Type: schema.TypeString, + Description: "TLS client certificate as PEM string for mutual authentication", + Default: "", + }, + "tls_client_private_key": { + Optional: true, + Type: schema.TypeString, + Description: "TLS client private key as PEM string for mutual authentication", + Default: "", + }, "red_hat_sso": { Optional: true, Type: schema.TypeBool, @@ -210,6 +222,8 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider { initialLogin := data.Get("initial_login").(bool) clientTimeout := data.Get("client_timeout").(int) tlsInsecureSkipVerify := data.Get("tls_insecure_skip_verify").(bool) + tlsClientCertificate := data.Get("tls_client_certificate").(string) + tlsClientPrivateKey := data.Get("tls_client_private_key").(string) rootCaCertificate := data.Get("root_ca_certificate").(string) redHatSSO := data.Get("red_hat_sso").(bool) additionalHeaders := make(map[string]string) @@ -221,7 +235,7 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider { userAgent := fmt.Sprintf("HashiCorp Terraform/%s (+https://www.terraform.io) Terraform Plugin SDK/%s", provider.TerraformVersion, meta.SDKVersionString()) - keycloakClient, err := keycloak.NewKeycloakClient(ctx, url, basePath, clientId, clientSecret, realm, username, password, initialLogin, clientTimeout, rootCaCertificate, tlsInsecureSkipVerify, userAgent, redHatSSO, additionalHeaders) + keycloakClient, err := keycloak.NewKeycloakClient(ctx, url, basePath, clientId, clientSecret, realm, username, password, initialLogin, clientTimeout, rootCaCertificate, tlsInsecureSkipVerify, userAgent, redHatSSO, additionalHeaders, tlsClientCertificate, tlsClientPrivateKey) if err != nil { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, diff --git a/provider/provider_test.go b/provider/provider_test.go index 04c45eb4c..ba32841fc 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -3,21 +3,24 @@ package provider import ( "context" "fmt" + "os" + "testing" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/meta" - "github.com/mrparkers/terraform-provider-keycloak/keycloak" - "os" - "testing" + "github.com/mrkparkers/terraform-provider-keycloak/keycloak" ) -var testAccProviderFactories map[string]func() (*schema.Provider, error) -var testAccProvider *schema.Provider -var keycloakClient *keycloak.KeycloakClient -var testAccRealm *keycloak.Realm -var testAccRealmTwo *keycloak.Realm -var testAccRealmUserFederation *keycloak.Realm -var testCtx context.Context +var ( + testAccProviderFactories map[string]func() (*schema.Provider, error) + testAccProvider *schema.Provider + keycloakClient *keycloak.KeycloakClient + testAccRealm *keycloak.Realm + testAccRealmTwo *keycloak.Realm + testAccRealmUserFederation *keycloak.Realm + testCtx context.Context +) var requiredEnvironmentVariables = []string{ "KEYCLOAK_CLIENT_ID", @@ -31,7 +34,7 @@ func init() { userAgent := fmt.Sprintf("HashiCorp Terraform/%s (+https://www.terraform.io) Terraform Plugin SDK/%s", schema.Provider{}.TerraformVersion, meta.SDKVersionString()) keycloakClient, _ = keycloak.NewKeycloakClient(testCtx, os.Getenv("KEYCLOAK_URL"), "", os.Getenv("KEYCLOAK_CLIENT_ID"), os.Getenv("KEYCLOAK_CLIENT_SECRET"), os.Getenv("KEYCLOAK_REALM"), "", "", true, 5, "", false, userAgent, false, map[string]string{ "foo": "bar", - }) + }, "", "") testAccProvider = KeycloakProvider(keycloakClient) testAccProviderFactories = map[string]func() (*schema.Provider, error){ "keycloak": func() (*schema.Provider, error) { From dfc89978cb44f5d721967900e9e4b2a6f4379c54 Mon Sep 17 00:00:00 2001 From: lucdew Date: Wed, 12 Jun 2024 15:51:23 +0200 Subject: [PATCH 2/2] Fix package import in provider_test.go --- provider/provider_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider/provider_test.go b/provider/provider_test.go index ba32841fc..15a17a306 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/meta" - "github.com/mrkparkers/terraform-provider-keycloak/keycloak" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" ) var (