Skip to content

Commit

Permalink
new resources: keycloak_identity_provider and mappers (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewChubatiuk authored and mrparkers committed Apr 18, 2019
1 parent 4e09fae commit e899c32
Show file tree
Hide file tree
Showing 22 changed files with 3,232 additions and 228 deletions.
511 changes: 306 additions & 205 deletions example/main.tf

Large diffs are not rendered by default.

82 changes: 82 additions & 0 deletions keycloak/identity_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package keycloak

import (
"fmt"
"log"
)

type IdentityProviderConfig struct {
Key string `json:"key,omitempty"`
HostIp string `json:"hostIp,omitempty"`
UseJwksUrl KeycloakBoolQuoted `json:"useJwksUrl,omitempty"`
ClientId string `json:"clientId,omitempty"`
ClientSecret string `json:"clientSecret,omitempty"`
DisableUserInfo KeycloakBoolQuoted `json:"disableUserInfo"`
HideOnLoginPage KeycloakBoolQuoted `json:"hideOnLoginPage"`
NameIDPolicyFormat string `json:"nameIDPolicyFormat,omitempty"`
SingleLogoutServiceUrl string `json:"singleLogoutServiceUrl,omitempty"`
SingleSignOnServiceUrl string `json:"singleSignOnServiceUrl,omitempty"`
SigningCertificate string `json:"signingCertificate,omitempty"`
SignatureAlgorithm string `json:"signatureAlgorithm,omitempty"`
XmlSignKeyInfoKeyNameTransformer string `json:"xmlSignKeyInfoKeyNameTransformer,omitempty"`
PostBindingAuthnRequest KeycloakBoolQuoted `json:"postBindingAuthnRequest,omitempty"`
PostBindingResponse KeycloakBoolQuoted `json:"postBindingResponse,omitempty"`
PostBindingLogout KeycloakBoolQuoted `json:"postBindingLogout,omitempty"`
ForceAuthn KeycloakBoolQuoted `json:"forceAuthn,omitempty"`
WantAuthnRequestsSigned KeycloakBoolQuoted `json:"wantAuthnRequestsSigned,omitempty"`
WantAssertionsSigned KeycloakBoolQuoted `json:"wantAssertionsSigned,omitempty"`
WantAssertionsEncrypted KeycloakBoolQuoted `json:"wantAssertionsEncrypted,omitempty"`
BackchannelSupported KeycloakBoolQuoted `json:"backchannelSupported,omitempty"`
ValidateSignature KeycloakBoolQuoted `json:"validateSignature,omitempty"`
AuthorizationUrl string `json:"authorizationUrl,omitempty"`
TokenUrl string `json:"tokenUrl,omitempty"`
LoginHint string `json:"loginHint,omitempty"`
UILocales KeycloakBoolQuoted `json:"uiLocales,omitempty"`
}

type IdentityProvider struct {
Realm string `json:"-"`
InternalId string `json:"internalId,omitempty"`
Alias string `json:"alias"`
DisplayName string `json:"displayName"`
ProviderId string `json:"providerId"`
Enabled bool `json:"enabled"`
StoreToken bool `json:"storeToken"`
AddReadTokenRoleOnCreate bool `json:"addReadTokenRoleOnCreate"`
AuthenticateByDefault bool `json:"authenticateByDefault"`
LinkOnly bool `json:"linkOnly"`
TrustEmail bool `json:"trustEmail"`
FirstBrokerLoginFlowAlias string `json:"firstBrokerLoginFlowAlias"`
PostBrokerLoginFlowAlias string `json:"postBrokerLoginFlowAlias"`
Config *IdentityProviderConfig `json:"config"`
}

func (keycloakClient *KeycloakClient) NewIdentityProvider(identityProvider *IdentityProvider) error {
log.Printf("[WARN] Realm: %s", identityProvider.Realm)
_, err := keycloakClient.post(fmt.Sprintf("/realms/%s/identity-provider/instances", identityProvider.Realm), identityProvider)
if err != nil {
return err
}

return nil
}

func (keycloakClient *KeycloakClient) GetIdentityProvider(realm, alias string) (*IdentityProvider, error) {
var identityProvider IdentityProvider
identityProvider.Realm = realm

err := keycloakClient.get(fmt.Sprintf("/realms/%s/identity-provider/instances/%s", realm, alias), &identityProvider)
if err != nil {
return nil, err
}

return &identityProvider, nil
}

func (keycloakClient *KeycloakClient) UpdateIdentityProvider(identityProvider *IdentityProvider) error {
return keycloakClient.put(fmt.Sprintf("/realms/%s/identity-provider/instances/%s", identityProvider.Realm, identityProvider.Alias), identityProvider)
}

func (keycloakClient *KeycloakClient) DeleteIdentityProvider(realm, alias string) error {
return keycloakClient.delete(fmt.Sprintf("/realms/%s/identity-provider/instances/%s", realm, alias))
}
61 changes: 61 additions & 0 deletions keycloak/identity_provider_mapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package keycloak

import (
"fmt"
"log"
)

type IdentityProviderMapperConfig struct {
UserAttribute string `json:"user.attribute,omitempty"`
Claim string `json:"claim,omitempty"`
ClaimValue string `json:"claim.value,omitempty"`
HardcodedAttribute string `json:"attribute,omitempty"`
Attribute string `json:"attribute.name,omitempty"`
AttributeValue string `json:"attribute.value,omitempty"`
AttributeFriendlyName string `json:"attribute.friendly.name,omitempty"`
Template string `json:"template,omitempty"`
Role string `json:"role,omitempty"`
}

type IdentityProviderMapper struct {
Realm string `json:"-"`
Provider string `json:"-"`
Id string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
IdentityProviderAlias string `json:"identityProviderAlias,omitempty"`
IdentityProviderMapper string `json:"identityProviderMapper,omitempty"`
Config *IdentityProviderMapperConfig `json:"config,omitempty"`
}

func (keycloakClient *KeycloakClient) NewIdentityProviderMapper(identityProviderMapper *IdentityProviderMapper) error {
log.Printf("[WARN] Realm: %s", identityProviderMapper.Realm)
location, err := keycloakClient.post(fmt.Sprintf("/realms/%s/identity-provider/instances/%s/mappers", identityProviderMapper.Realm, identityProviderMapper.IdentityProviderAlias), identityProviderMapper)
if err != nil {
return err
}

identityProviderMapper.Id = getIdFromLocationHeader(location)

return nil
}

func (keycloakClient *KeycloakClient) GetIdentityProviderMapper(realm, alias, id string) (*IdentityProviderMapper, error) {
var identityProviderMapper IdentityProviderMapper
identityProviderMapper.Realm = realm
identityProviderMapper.IdentityProviderAlias = alias

err := keycloakClient.get(fmt.Sprintf("/realms/%s/identity-provider/instances/%s/mappers/%s", realm, alias, id), &identityProviderMapper)
if err != nil {
return nil, err
}

return &identityProviderMapper, nil
}

func (keycloakClient *KeycloakClient) UpdateIdentityProviderMapper(identityProviderMapper *IdentityProviderMapper) error {
return keycloakClient.put(fmt.Sprintf("/realms/%s/identity-provider/instances/%s/mappers/%s", identityProviderMapper.Realm, identityProviderMapper.IdentityProviderAlias, identityProviderMapper.Id), identityProviderMapper)
}

func (keycloakClient *KeycloakClient) DeleteIdentityProviderMapper(realm, alias, id string) error {
return keycloakClient.delete(fmt.Sprintf("/realms/%s/identity-provider/instances/%s/mappers/%s", realm, alias, id))
}
33 changes: 33 additions & 0 deletions keycloak/util.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,44 @@
package keycloak

import (
"bytes"
"strconv"
"strings"
"time"
)

type KeycloakBoolQuoted bool

func (c KeycloakBoolQuoted) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
if c == false {
buf.WriteString(`""`)
} else {
buf.WriteString(strconv.Quote(strconv.FormatBool(bool(c))))
}
return buf.Bytes(), nil
}

func (c *KeycloakBoolQuoted) UnmarshalJSON(in []byte) error {
value := string(in)
if value == `""` {
*c = false
return nil
}
unquoted, err := strconv.Unquote(value)
if err != nil {
return err
}
var b bool
b, err = strconv.ParseBool(unquoted)
if err != nil {
return err
}
res := KeycloakBoolQuoted(b)
*c = res
return nil
}

func getIdFromLocationHeader(locationHeader string) string {
parts := strings.Split(locationHeader, "/")

Expand Down
195 changes: 195 additions & 0 deletions provider/generic_keycloak_identity_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package provider

import (
"fmt"
"github.com/hashicorp/terraform/helper/schema"
"github.com/mrparkers/terraform-provider-keycloak/keycloak"
"strings"
)

type identityProviderDataGetterFunc func(data *schema.ResourceData) (*keycloak.IdentityProvider, error)
type identityProviderDataSetterFunc func(data *schema.ResourceData, identityProvider *keycloak.IdentityProvider) error

func resourceKeycloakIdentityProvider() *schema.Resource {
return &schema.Resource{
Delete: resourceKeycloakIdentityProviderDelete,
Importer: &schema.ResourceImporter{
State: resourceKeycloakIdentityProviderImport,
},
Schema: map[string]*schema.Schema{
"alias": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "The alias uniquely identifies an identity provider and it is also used to build the redirect uri.",
},
"realm": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Realm Name",
},
"internal_id": {
Type: schema.TypeString,
Computed: true,
Description: "Internal Identity Provider Id",
},
"display_name": {
Type: schema.TypeString,
Optional: true,
Default: "",
Description: "Friendly name for Identity Providers.",
},
"enabled": {
Type: schema.TypeBool,
Optional: true,
Default: true,
Description: "Enable/disable this identity provider.",
},
"store_token": {
Type: schema.TypeBool,
Optional: true,
Default: true,
Description: "Enable/disable if tokens must be stored after authenticating users.",
},
"add_read_token_role_on_create": {
Type: schema.TypeBool,
Optional: true,
Default: false,
ForceNew: true,
Description: "Enable/disable if new users can read any stored tokens. This assigns the broker.read-token role.",
},
"authenticate_by_default": {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "Enable/disable authenticate users by default.",
},
"link_only": {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "If true, users cannot log in through this provider. They can only link to this provider. This is useful if you don't want to allow login from the provider, but want to integrate with a provider",
},
"trust_email": {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "If enabled then email provided by this provider is not verified even if verification is enabled for the realm.",
},
"first_broker_login_flow_alias": {
Type: schema.TypeString,
Optional: true,
Default: "first broker login",
Description: "Alias of authentication flow, which is triggered after first login with this identity provider. Term 'First Login' means that there is not yet existing Keycloak account linked with the authenticated identity provider account.",
},
"post_broker_login_flow_alias": {
Type: schema.TypeString,
Optional: true,
Default: "",
Description: "Alias of authentication flow, which is triggered after each login with this identity provider. Useful if you want additional verification of each user authenticated with this identity provider (for example OTP). Leave this empty if you don't want any additional authenticators to be triggered after login with this identity provider. Also note, that authenticator implementations must assume that user is already set in ClientSession as identity provider already set it.",
},
},
}
}

func getIdentityProviderFromData(data *schema.ResourceData) (*keycloak.IdentityProvider, error) {
rec := &keycloak.IdentityProvider{
Realm: data.Get("realm").(string),
Alias: data.Get("alias").(string),
DisplayName: data.Get("display_name").(string),
Enabled: data.Get("enabled").(bool),
StoreToken: data.Get("store_token").(bool),
AddReadTokenRoleOnCreate: data.Get("add_read_token_role_on_create").(bool),
AuthenticateByDefault: data.Get("authenticate_by_default").(bool),
LinkOnly: data.Get("link_only").(bool),
TrustEmail: data.Get("trust_email").(bool),
FirstBrokerLoginFlowAlias: data.Get("first_broker_login_flow_alias").(string),
PostBrokerLoginFlowAlias: data.Get("post_broker_login_flow_alias").(string),
InternalId: data.Get("internal_id").(string),
}
return rec, nil
}

func setIdentityProviderData(data *schema.ResourceData, identityProvider *keycloak.IdentityProvider) error {
data.SetId(identityProvider.Alias)
data.Set("internal_id", identityProvider.InternalId)
data.Set("realm", identityProvider.Realm)
data.Set("alias", identityProvider.Alias)
data.Set("display_name", identityProvider.DisplayName)
data.Set("enabled", identityProvider.Enabled)
data.Set("store_token", identityProvider.StoreToken)
data.Set("add_read_token_role_on_create", identityProvider.AddReadTokenRoleOnCreate)
data.Set("authenticate_by_default", identityProvider.AuthenticateByDefault)
data.Set("link_only", identityProvider.LinkOnly)
data.Set("trust_email", identityProvider.TrustEmail)
data.Set("first_broker_login_flow_alias", identityProvider.FirstBrokerLoginFlowAlias)
data.Set("post_broker_login_flow_alias", identityProvider.PostBrokerLoginFlowAlias)
return nil
}

func resourceKeycloakIdentityProviderDelete(data *schema.ResourceData, meta interface{}) error {
keycloakClient := meta.(*keycloak.KeycloakClient)

realm := data.Get("realm").(string)
alias := data.Get("alias").(string)

return keycloakClient.DeleteIdentityProvider(realm, alias)
}

func resourceKeycloakIdentityProviderImport(d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
parts := strings.Split(d.Id(), "/")

if len(parts) != 2 {
return nil, fmt.Errorf("Invalid import. Supported import formats: {{realm}}/{{identityProviderAlias}}")
}

d.Set("realm", parts[0])
d.SetId(parts[1])

return []*schema.ResourceData{d}, nil
}

func resourceKeycloakIdentityProviderCreate(getIdentityProviderFromData identityProviderDataGetterFunc, setDataFromIdentityProvider identityProviderDataSetterFunc) func(data *schema.ResourceData, meta interface{}) error {
return func(data *schema.ResourceData, meta interface{}) error {
keycloakClient := meta.(*keycloak.KeycloakClient)
identityProvider, err := getIdentityProviderFromData(data)
if err = keycloakClient.NewIdentityProvider(identityProvider); err != nil {
return err
}
if err = setDataFromIdentityProvider(data, identityProvider); err != nil {
return err
}
return resourceKeycloakIdentityProviderRead(setDataFromIdentityProvider)(data, meta)
}
}

func resourceKeycloakIdentityProviderRead(setDataFromIdentityProvider identityProviderDataSetterFunc) func(data *schema.ResourceData, meta interface{}) error {
return func(data *schema.ResourceData, meta interface{}) error {
keycloakClient := meta.(*keycloak.KeycloakClient)
realm := data.Get("realm").(string)
alias := data.Get("alias").(string)
identityProvider, err := keycloakClient.GetIdentityProvider(realm, alias)
if err != nil {
return handleNotFoundError(err, data)
}
if err = setDataFromIdentityProvider(data, identityProvider); err != nil {
return err
}
return nil
}
}

func resourceKeycloakIdentityProviderUpdate(getIdentityProviderFromData identityProviderDataGetterFunc, setDataFromIdentityProvider identityProviderDataSetterFunc) func(data *schema.ResourceData, meta interface{}) error {
return func(data *schema.ResourceData, meta interface{}) error {
keycloakClient := meta.(*keycloak.KeycloakClient)
identityProvider, err := getIdentityProviderFromData(data)
if err = keycloakClient.UpdateIdentityProvider(identityProvider); err != nil {
return err
}
if err = setDataFromIdentityProvider(data, identityProvider); err != nil {
return err
}
return nil
}
}
Loading

0 comments on commit e899c32

Please sign in to comment.