From 04fac894350291b0b48cd7d2b7d0d4e0174d632c Mon Sep 17 00:00:00 2001 From: Olivier LANIESSE Date: Tue, 20 Apr 2021 13:54:41 +0200 Subject: [PATCH] feat(keycloak) : Add datasource keycloak_client_description_converter Signed-off-by: Olivier LANIESSE --- .../generic_client_description_converter.go | 63 +++++ keycloak/keycloak_client.go | 13 + ...e_keycloak_client_description_converter.go | 249 ++++++++++++++++++ ...cloak_client_description_converter_test.go | 71 +++++ provider/provider.go | 1 + 5 files changed, 397 insertions(+) create mode 100644 keycloak/generic_client_description_converter.go create mode 100644 provider/data_source_keycloak_client_description_converter.go create mode 100644 provider/data_source_keycloak_client_description_converter_test.go diff --git a/keycloak/generic_client_description_converter.go b/keycloak/generic_client_description_converter.go new file mode 100644 index 000000000..196ac82a1 --- /dev/null +++ b/keycloak/generic_client_description_converter.go @@ -0,0 +1,63 @@ +package keycloak + +import ( + "encoding/json" + "fmt" +) + +type GenericClientRepresentation struct { + Access map[string]string `json:"access"` + AdminUrl string `json:"adminUrl"` + Attributes map[string]string `json:"attributes"` + AuthenticationFlowBindingOverrides map[string]string `json:"authenticationFlowBindingOverrides"` + AuthorizationServicesEnabled bool `json:"authorizationServicesEnabled"` + AuthorizationSettings map[string]string `json:"authorizationSettings"` + BaseUrl string `json:"baseUrl"` + BearerOnly bool `json:"bearerOnly"` + ClientAuthenticatorType string `json:"clientAuthenticatorType"` + ClientId string `json:"clientId"` + ConsentRequired string `json:"consentRequired"` + DefaultClientScopes []string `json:"defaultClientScopes"` + DefaultRoles []string `json:"defaultRoles"` + Description string `json:"description"` + DirectAccessGrantsEnabled bool `json:"directAccessGrantsEnabled"` + Enabled bool `json:"enabled"` + FrontchannelLogout bool `json:"frontchannelLogout"` + FullScopeAllowed bool `json:"fullScopeAllowed"` + Id string `json:"id"` + ImplicitFlowEnabled bool `json:"implicitFlowEnabled"` + Name string `json:"name"` + NotBefore int `json:"notBefore"` + OptionalClientScopes []string `json:"optionalClientScopes"` + Origin string `json:"origin"` + Protocol string `json:"protocol"` + ProtocolMappers []*GenericClientProtocolMapper `json:"protocolMappers"` + PublicClient bool `json:"publicClient"` + RedirectUris []string `json:"redirectUris"` + RegisteredNodes map[string]string `json:"registeredNodes"` + RegistrationAccessToken string `json:"registrationAccessToken"` + RootUrl string `json:"rootUrl"` + Secret string `json:"secret"` + ServiceAccountsEnabled bool `json:"serviceAccountsEnabled"` + StandardFlowEnabled bool `json:"standardFlowEnabled"` + SurrogateAuthRequired bool `json:"surrogateAuthRequired"` + WebOrigins []string `json:"webOrigins"` +} + +func (keycloakClient *KeycloakClient) NewGenericClientDescription(realmId string, body string) (*GenericClientRepresentation, error) { + var genericClientRepresentation GenericClientRepresentation + + result, err := keycloakClient.sendRaw(fmt.Sprintf("/realms/%s/client-description-converter", realmId), []byte(body)) + + if err != nil { + return nil, err + } + + err = json.Unmarshal(result, &genericClientRepresentation) + + if err != nil { + return nil, err + } + + return &genericClientRepresentation, nil +} diff --git a/keycloak/keycloak_client.go b/keycloak/keycloak_client.go index 4f41935be..aa863255f 100644 --- a/keycloak/keycloak_client.go +++ b/keycloak/keycloak_client.go @@ -371,6 +371,19 @@ func (keycloakClient *KeycloakClient) getRaw(path string, params map[string]stri return body, err } +func (keycloakClient *KeycloakClient) sendRaw(path string, requestBody []byte) ([]byte, error) { + resourceUrl := keycloakClient.baseUrl + apiUrl + path + + request, err := http.NewRequest(http.MethodPost, resourceUrl, nil) + if err != nil { + return nil, err + } + + body, _, err := keycloakClient.sendRequest(request, requestBody) + + return body, err +} + func (keycloakClient *KeycloakClient) post(path string, requestBody interface{}) ([]byte, string, error) { resourceUrl := keycloakClient.baseUrl + apiUrl + path diff --git a/provider/data_source_keycloak_client_description_converter.go b/provider/data_source_keycloak_client_description_converter.go new file mode 100644 index 000000000..45e8a9c21 --- /dev/null +++ b/provider/data_source_keycloak_client_description_converter.go @@ -0,0 +1,249 @@ +package provider + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" +) + +func dataSourceKeycloakClientDescriptionConverter() *schema.Resource { + return &schema.Resource{ + Read: dataSourceKeycloakClientDescriptionConverterRead, + + Schema: map[string]*schema.Schema{ + "realm_id": { + Type: schema.TypeString, + Required: true, + }, + "body": { + Type: schema.TypeString, + Required: true, + }, + "access": { + Type: schema.TypeMap, + Computed: true, + }, + "admin_url": { + Type: schema.TypeString, + Computed: true, + }, + "attributes": { + Type: schema.TypeMap, + Computed: true, + }, + "authentication_flow_binding_overrides": { + Type: schema.TypeMap, + Computed: true, + }, + "authorization_services_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "authorization_settings": { + Type: schema.TypeMap, + Computed: true, + }, + "base_url": { + Type: schema.TypeString, + Computed: true, + }, + "bearer_only": { + Type: schema.TypeBool, + Computed: true, + }, + "client_authenticator_type": { + Type: schema.TypeString, + Computed: true, + }, + "client_id": { + Type: schema.TypeString, + Computed: true, + }, + "consent_required": { + Type: schema.TypeString, + Computed: true, + }, + "default_client_scopes": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + "default_roles": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "direct_access_grants_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "frontchannel_logout": { + Type: schema.TypeBool, + Computed: true, + }, + "full_scope_allowed": { + Type: schema.TypeBool, + Computed: true, + }, + "implicit_flow_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "not_before": { + Type: schema.TypeInt, + Computed: true, + }, + "optional_client_scopes": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + "origin": { + Type: schema.TypeString, + Computed: true, + }, + "protocol": { + Type: schema.TypeString, + Computed: true, + }, + "protocol_mappers": { + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "protocol": { + Type: schema.TypeString, + Computed: true, + }, + "protocol_mapper": { + Type: schema.TypeString, + Computed: true, + }, + "config": { + Type: schema.TypeMap, + Computed: true, + }, + }, + }, + Computed: true, + }, + "public_client": { + Type: schema.TypeBool, + Computed: true, + }, + "redirect_uris": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + "registered_nodes": { + Type: schema.TypeMap, + Computed: true, + }, + "registration_access_token": { + Type: schema.TypeString, + Computed: true, + }, + "root_url": { + Type: schema.TypeString, + Computed: true, + }, + "secret": { + Type: schema.TypeString, + Computed: true, + }, + "service_accounts_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "standard_flow_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "surrogate_auth_required": { + Type: schema.TypeBool, + Computed: true, + }, + "web_origins": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + }, + } +} + +func setClientDescriptionConverterData(data *schema.ResourceData, description *keycloak.GenericClientRepresentation) { + data.SetId(description.ClientId) + + data.Set("access", description.Access) + data.Set("admin_url", description.AdminUrl) + data.Set("attributes", description.Attributes) + data.Set("authentication_flow_binding_overrides", description.AuthenticationFlowBindingOverrides) + data.Set("authorization_services_enabled", description.AuthorizationServicesEnabled) + data.Set("authorization_settings", description.AuthorizationSettings) + data.Set("base_url", description.BaseUrl) + data.Set("bearer_only", description.BearerOnly) + data.Set("client_authenticator_type", description.ClientAuthenticatorType) + data.Set("client_id", description.ClientId) + data.Set("consent_required", description.ConsentRequired) + data.Set("default_client_scopes", description.DefaultClientScopes) + data.Set("default_roles", description.DefaultRoles) + data.Set("description", description.Description) + data.Set("direct_access_grants_enabled", description.DirectAccessGrantsEnabled) + data.Set("enabled", description.Enabled) + data.Set("frontchannel_logout", description.FrontchannelLogout) + data.Set("full_scope_allowed", description.FullScopeAllowed) + data.Set("implicit_flow_enabled", description.ImplicitFlowEnabled) + data.Set("name", description.Name) + data.Set("not_before", description.NotBefore) + data.Set("optional_client_scopes", description.OptionalClientScopes) + data.Set("origin", description.Origin) + data.Set("protocol", description.Protocol) + data.Set("protocol_mappers", description.ProtocolMappers) + data.Set("public_client", description.PublicClient) + data.Set("redirect_uris", description.RedirectUris) + data.Set("registered_nodes", description.RegisteredNodes) + data.Set("registration_access_token", description.RegistrationAccessToken) + data.Set("root_url", description.RootUrl) + data.Set("secret", description.Secret) + data.Set("service_accounts_enabled", description.ServiceAccountsEnabled) + data.Set("standard_flow_enabled", description.StandardFlowEnabled) + data.Set("surrogate_auth_required", description.SurrogateAuthRequired) + data.Set("web_origins", description.WebOrigins) +} + +func dataSourceKeycloakClientDescriptionConverterRead(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + body := data.Get("body").(string) + + description, err := keycloakClient.NewGenericClientDescription(realmId, body) + + if err != nil { + return handleNotFoundError(err, data) + } + + setClientDescriptionConverterData(data, description) + + return nil +} diff --git a/provider/data_source_keycloak_client_description_converter_test.go b/provider/data_source_keycloak_client_description_converter_test.go new file mode 100644 index 000000000..8c4a26540 --- /dev/null +++ b/provider/data_source_keycloak_client_description_converter_test.go @@ -0,0 +1,71 @@ +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccKeycloakDataSourceClientDescriptionConverter_basic(t *testing.T) { + t.Parallel() + clientId := acctest.RandomWithPrefix("tf-acc-test") + dataSourceName := "data.keycloak_client_description_converter.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccKeycloakClientDescriptionConverterConfig(clientId), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "protocol", "saml"), + resource.TestCheckResourceAttr(dataSourceName, "client_id", "FakeEntityId"), + resource.TestCheckResourceAttr(dataSourceName, "realm_id", testAccRealm.Realm), + ), + }, + }, + }) +} + +func testAccKeycloakClientDescriptionConverterConfig(clientId string) string { + return fmt.Sprintf(` +data "keycloak_realm" "realm" { + realm = "%s" +} + +data "keycloak_client_description_converter" "test" { + realm_id = data.keycloak_realm.realm.id + body = < + + + + + MIICyDCCAjGgAwIBAgIBADANBgkqhkiG9w0BAQ0FADCBgDELMAkGA1UEBhMCdXMx + CzAJBgNVBAgMAklBMSQwIgYDVQQKDBt0ZXJyYWZvcm0tcHJvdmlkZXIta2V5Y2xv + YWsxHDAaBgNVBAMME21ycGFya2Vycy5naXRodWIuaW8xIDAeBgkqhkiG9w0BCQEW + EW1pY2hhZWxAcGFya2VyLmdnMB4XDTE5MDEwODE0NDYzNloXDTI5MDEwNTE0NDYz + NlowgYAxCzAJBgNVBAYTAnVzMQswCQYDVQQIDAJJQTEkMCIGA1UECgwbdGVycmFm + b3JtLXByb3ZpZGVyLWtleWNsb2FrMRwwGgYDVQQDDBNtcnBhcmtlcnMuZ2l0aHVi + LmlvMSAwHgYJKoZIhvcNAQkBFhFtaWNoYWVsQHBhcmtlci5nZzCBnzANBgkqhkiG + 9w0BAQEFAAOBjQAwgYkCgYEAxuZny7uyYxGVPtpie14gNQC4tT9sAvO2sVNDhuoe + qIKLRpNwkHnwQmwe5OxSh9K0BPHp/DNuuVWUqvo4tniEYn3jBr7FwLYLTKojQIxj + 53S1UTT9EXq3eP5HsHMD0QnTuca2nlNYUDBm6ud2fQj0Jt5qLx86EbEC28N56IRv + GX8CAwEAAaNQME4wHQYDVR0OBBYEFMLnbQh77j7vhGTpAhKpDhCrBsPZMB8GA1Ud + IwQYMBaAFMLnbQh77j7vhGTpAhKpDhCrBsPZMAwGA1UdEwQFMAMBAf8wDQYJKoZI + hvcNAQENBQADgYEAB8wGrAQY0pAfwbnYSyBt4STbebeRTu1/q1ucfrtc3qsegcd5 + n01xTR+T2uZJwqHFPpFjr4IPORiHx3+4BWCweslPD53qBjKUPXcbMO1Revjef6Tj + K3K0AuJ94fxgXVoT61Nzu/a6Lj6RhzU/Dao9mlSbJY+YSbm+ZBpsuRUQ84s= + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + + + EOF +} +`, testAccRealm.Realm) +} diff --git a/provider/provider.go b/provider/provider.go index 56a19d1d9..5abb6b04b 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -25,6 +25,7 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider { "keycloak_saml_client": dataSourceKeycloakSamlClient(), "keycloak_authentication_execution": dataSourceKeycloakAuthenticationExecution(), "keycloak_authentication_flow": dataSourceKeycloakAuthenticationFlow(), + "keycloak_client_description_converter": dataSourceKeycloakClientDescriptionConverter(), }, ResourcesMap: map[string]*schema.Resource{ "keycloak_realm": resourceKeycloakRealm(),