Skip to content

Commit

Permalink
feat: add support for keycloak version up to current (26.0.7)
Browse files Browse the repository at this point in the history
Signed-off-by: Sven Schumann <s.schumann@qvest-digital.com>
Co-authored-by: Markus Seidl <m.seidl@qvest-digital.com>
  • Loading branch information
sschum and markus-qvest-seidl committed Dec 10, 2024
1 parent f87470c commit 3cebc1c
Show file tree
Hide file tree
Showing 26 changed files with 380 additions and 93 deletions.
12 changes: 8 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,12 @@ jobs:
strategy:
matrix:
keycloak-version:
- '21.0.1'
- '20.0.5'
- '19.0.2'
- '26.0.7'
- '25.0.2'
- '24.0.5'
- '23.0.7'
- '22.0.5'
- '21.1.2'
fail-fast: false
concurrency:
group: ${{ github.head_ref || github.run_id }}-${{ matrix.keycloak-version }}
Expand Down Expand Up @@ -108,12 +111,13 @@ jobs:
return process.env.KEYCLOAK_VERSION.split("-")[0]
- name: Test
run: |
terraform version
go mod download
make testacc
env:
KEYCLOAK_CLIENT_ID: terraform
KEYCLOAK_CLIENT_SECRET: 884e0f95-0f42-4a63-9b1f-94274655669e
KEYCLOAK_CLIENT_TIMEOUT: 30
KEYCLOAK_CLIENT_TIMEOUT: 120
KEYCLOAK_REALM: master
KEYCLOAK_URL: "http://localhost:8080"
KEYCLOAK_TEST_PASSWORD_GRANT: "true"
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ site/
*.zip

.DS_Store

test_env.json
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## 4.5.1 ()

FEATURES:

- unit tests are now working with 26. ([#9](https://github.com/qvest-digital/terraform-provider-keycloak/pull/9))
- unit tests are now working from 21 to 25. ([#7](https://github.com/qvest-digital/terraform-provider-keycloak/pull/7))
- Please check IdP provider sync mode as the default has changed to "LEGACY"
- Keycloak 25: SAML clients have a default 'saml_organization'. If 'saml_organization' isn't specified in the provider configuration, the provider will delete this scope.


## 4.5.0 (December 6, 2024)

IMPROVEMENTS:
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,12 @@ This provider will officially support the latest three major versions of Keycloa

The following versions are used when running acceptance tests in CI:

- 21.0.1 (latest)
- 20.0.5
- 19.0.2
- 26.0.7 (latest)
- 25.0.2
- 24.0.5
- 23.0.7
- 22.0.5
- 21.1.2

## Releases

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ services:
environment:
LDAP_PORT_NUMBER: 389
keycloak:
image: quay.io/keycloak/keycloak:21.0.1
image: quay.io/keycloak/keycloak:26.0.7
command: --verbose start-dev --features=preview
depends_on:
- postgres
Expand Down
3 changes: 2 additions & 1 deletion keycloak/identity_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type IdentityProviderConfig struct {
ClientSecret string `json:"clientSecret,omitempty"`
DisableUserInfo types.KeycloakBoolQuoted `json:"disableUserInfo"`
UserInfoUrl string `json:"userInfoUrl,omitempty"`
HideOnLoginPage types.KeycloakBoolQuoted `json:"hideOnLoginPage"`
HideOnLoginPage types.KeycloakBoolQuoted `json:"hideOnLoginPage,omitempty"`
NameIDPolicyFormat string `json:"nameIDPolicyFormat,omitempty"`
EntityId string `json:"entityId,omitempty"`
SingleLogoutServiceUrl string `json:"singleLogoutServiceUrl,omitempty"`
Expand Down Expand Up @@ -65,6 +65,7 @@ type IdentityProvider struct {
AddReadTokenRoleOnCreate bool `json:"addReadTokenRoleOnCreate"`
AuthenticateByDefault bool `json:"authenticateByDefault"`
LinkOnly bool `json:"linkOnly"`
HideOnLogin bool `json:"hideOnLogin,omitempty"` //since keycloak v26
TrustEmail bool `json:"trustEmail"`
FirstBrokerLoginFlowAlias string `json:"firstBrokerLoginFlowAlias"`
PostBrokerLoginFlowAlias string `json:"postBrokerLoginFlowAlias"`
Expand Down
4 changes: 2 additions & 2 deletions keycloak/keycloak_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func (keycloakClient *KeycloakClient) login(ctx context.Context) error {
return nil
}

func (keycloakClient *KeycloakClient) refresh(ctx context.Context) error {
func (keycloakClient *KeycloakClient) Refresh(ctx context.Context) error {
refreshTokenUrl := fmt.Sprintf(tokenUrl, keycloakClient.baseUrl, keycloakClient.realm)
refreshTokenData := keycloakClient.getAuthenticationFormData()

Expand Down Expand Up @@ -340,7 +340,7 @@ func (keycloakClient *KeycloakClient) sendRequest(ctx context.Context, request *
"status": response.Status,
})

err := keycloakClient.refresh(ctx)
err := keycloakClient.Refresh(ctx)
if err != nil {
return nil, "", fmt.Errorf("error refreshing credentials: %s", err)
}
Expand Down
19 changes: 19 additions & 0 deletions keycloak/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,27 @@ const (
Version_17 Version = "17.0.0"
Version_18 Version = "18.0.0"
Version_19 Version = "19.0.0"
Version_20 Version = "20.0.0"
Version_21 Version = "21.0.0"
Version_22 Version = "22.0.0"
Version_23 Version = "23.0.0"
Version_24 Version = "24.0.0"
Version_25 Version = "25.0.0"
Version_26 Version = "26.0.0"
)

func (v Version) AsVersion() *version.Version {
vv, err := version.NewVersion(string(v))
if err != nil {
return nil
}
return vv
}

func (KeycloakClient *KeycloakClient) Version() *version.Version {
return KeycloakClient.version
}

func (keycloakClient *KeycloakClient) VersionIsGreaterThanOrEqualTo(ctx context.Context, versionString Version) (bool, error) {
if keycloakClient.version == nil {
err := keycloakClient.login(ctx)
Expand Down
38 changes: 26 additions & 12 deletions provider/generic_keycloak_identity_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package provider
import (
"context"
"fmt"
"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
Expand All @@ -17,8 +18,8 @@ var syncModes = []string{
"LEGACY",
}

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

func resourceKeycloakIdentityProvider() *schema.Resource {
return &schema.Resource{
Expand Down Expand Up @@ -114,23 +115,23 @@ func resourceKeycloakIdentityProvider() *schema.Resource {
"sync_mode": {
Type: schema.TypeString,
Optional: true,
Default: "",
Default: "LEGACY",
ValidateFunc: validation.StringInSlice(syncModes, false),
Description: "Sync Mode",
},
},
}
}

func getIdentityProviderFromData(data *schema.ResourceData) (*keycloak.IdentityProvider, *keycloak.IdentityProviderConfig) {
func getIdentityProviderFromData(data *schema.ResourceData, keycloakVersion *version.Version) (*keycloak.IdentityProvider, *keycloak.IdentityProviderConfig) {
// some identity provider config is shared among all identity providers, so this default config will be used as a base to merge extra config into
defaultIdentityProviderConfig := &keycloak.IdentityProviderConfig{
GuiOrder: data.Get("gui_order").(string),
SyncMode: data.Get("sync_mode").(string),
ExtraConfig: getExtraConfigFromData(data),
}

return &keycloak.IdentityProvider{
identityProvider := &keycloak.IdentityProvider{
Realm: data.Get("realm").(string),
Alias: data.Get("alias").(string),
DisplayName: data.Get("display_name").(string),
Expand All @@ -143,10 +144,16 @@ func getIdentityProviderFromData(data *schema.ResourceData) (*keycloak.IdentityP
FirstBrokerLoginFlowAlias: data.Get("first_broker_login_flow_alias").(string),
PostBrokerLoginFlowAlias: data.Get("post_broker_login_flow_alias").(string),
InternalId: data.Get("internal_id").(string),
}, defaultIdentityProviderConfig
}
if keycloakVersion.GreaterThanOrEqual(keycloak.Version_26.AsVersion()) {
// Since keycloak v26 the attribute is moved from Config to Provider.
identityProvider.HideOnLogin = data.Get("hide_on_login_page").(bool)
}

return identityProvider, defaultIdentityProviderConfig
}

func setIdentityProviderData(data *schema.ResourceData, identityProvider *keycloak.IdentityProvider) {
func setIdentityProviderData(data *schema.ResourceData, identityProvider *keycloak.IdentityProvider, keycloakVersion *version.Version) {
data.SetId(identityProvider.Alias)

data.Set("internal_id", identityProvider.InternalId)
Expand All @@ -162,6 +169,10 @@ func setIdentityProviderData(data *schema.ResourceData, identityProvider *keyclo
data.Set("first_broker_login_flow_alias", identityProvider.FirstBrokerLoginFlowAlias)
data.Set("post_broker_login_flow_alias", identityProvider.PostBrokerLoginFlowAlias)

if keycloakVersion.GreaterThanOrEqual(keycloak.Version_26.AsVersion()) {
data.Set("hide_on_login_page", identityProvider.HideOnLogin)
}

// identity provider config
data.Set("gui_order", identityProvider.Config.GuiOrder)
data.Set("sync_mode", identityProvider.Config.SyncMode)
Expand Down Expand Up @@ -194,15 +205,16 @@ func resourceKeycloakIdentityProviderImport(_ context.Context, d *schema.Resourc
func resourceKeycloakIdentityProviderCreate(getIdentityProviderFromData identityProviderDataGetterFunc, setDataFromIdentityProvider identityProviderDataSetterFunc) func(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
return func(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
keycloakClient := meta.(*keycloak.KeycloakClient)
identityProvider, err := getIdentityProviderFromData(data)
keycloakVersion := keycloakClient.Version()
identityProvider, err := getIdentityProviderFromData(data, keycloakVersion)
if err != nil {
return diag.FromErr(err)
}

if err = keycloakClient.NewIdentityProvider(ctx, identityProvider); err != nil {
return diag.FromErr(err)
}
if err = setDataFromIdentityProvider(data, identityProvider); err != nil {
if err = setDataFromIdentityProvider(data, identityProvider, keycloakVersion); err != nil {
return diag.FromErr(err)
}
return resourceKeycloakIdentityProviderRead(setDataFromIdentityProvider)(ctx, data, meta)
Expand All @@ -212,21 +224,23 @@ func resourceKeycloakIdentityProviderCreate(getIdentityProviderFromData identity
func resourceKeycloakIdentityProviderRead(setDataFromIdentityProvider identityProviderDataSetterFunc) func(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
return func(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
keycloakClient := meta.(*keycloak.KeycloakClient)
keycloakVersion := keycloakClient.Version()
realm := data.Get("realm").(string)
alias := data.Get("alias").(string)
identityProvider, err := keycloakClient.GetIdentityProvider(ctx, realm, alias)
if err != nil {
return handleNotFoundError(ctx, err, data)
}

return diag.FromErr(setDataFromIdentityProvider(data, identityProvider))
return diag.FromErr(setDataFromIdentityProvider(data, identityProvider, keycloakVersion))
}
}

func resourceKeycloakIdentityProviderUpdate(getIdentityProviderFromData identityProviderDataGetterFunc, setDataFromIdentityProvider identityProviderDataSetterFunc) func(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
return func(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
keycloakClient := meta.(*keycloak.KeycloakClient)
identityProvider, err := getIdentityProviderFromData(data)
keycloakVersion := keycloakClient.Version()
identityProvider, err := getIdentityProviderFromData(data, keycloakVersion)
if err != nil {
return diag.FromErr(err)
}
Expand All @@ -236,6 +250,6 @@ func resourceKeycloakIdentityProviderUpdate(getIdentityProviderFromData identity
return diag.FromErr(err)
}

return diag.FromErr(setDataFromIdentityProvider(data, identityProvider))
return diag.FromErr(setDataFromIdentityProvider(data, identityProvider, keycloakVersion))
}
}
52 changes: 46 additions & 6 deletions provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package provider

import (
"context"
"encoding/json"
"fmt"
"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/keycloak/terraform-provider-keycloak/keycloak"
"log"
"os"
"testing"
"time"
)

var testAccProviderFactories map[string]func() (*schema.Provider, error)
Expand All @@ -29,9 +32,36 @@ var requiredEnvironmentVariables = []string{
func init() {
testCtx = context.Background()
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{
var err error
// Load environment variables from a json file if it exists
// This is useful for running tests locally

if _, err := os.Stat("../test_env.json"); err == nil {
println("Using test_env.json to load environment variables...")
file, err := os.Open("../test_env.json")
if err != nil {
log.Fatalf("Unable to open env.json: %s", err)
}
defer file.Close()

var envVars map[string]string
if err := json.NewDecoder(file).Decode(&envVars); err != nil {
log.Fatalf("Unable to decode env.json: %s", err)
}

for key, value := range envVars {
if err := os.Setenv(key, value); err != nil {
log.Fatalf("Unable to set environment variable %s: %s", key, err)
}
}
}

keycloakClient, err = 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",
})
if err != nil {
panic(err)
}
testAccProvider = KeycloakProvider(keycloakClient)
testAccProviderFactories = map[string]func() (*schema.Provider, error){
"keycloak": func() (*schema.Provider, error) {
Expand All @@ -47,19 +77,20 @@ func TestMain(m *testing.M) {

code := m.Run()

// Clean up of tests is not fatal if it fails
err := keycloakClient.DeleteRealm(testCtx, testAccRealm.Realm)
if err != nil {
os.Exit(1)
log.Printf("Unable to delete realm %s: %s", testAccRealmUserFederation.Realm, err)
}

err = keycloakClient.DeleteRealm(testCtx, testAccRealmTwo.Realm)
if err != nil {
os.Exit(1)
log.Printf("Unable to delete realm %s: %s", testAccRealmUserFederation.Realm, err)
}

err = keycloakClient.DeleteRealm(testCtx, testAccRealmUserFederation.Realm)
if err != nil {
os.Exit(1)
log.Printf("Unable to delete realm %s: %s", testAccRealmUserFederation.Realm, err)
}

os.Exit(code)
Expand All @@ -73,9 +104,18 @@ func createTestRealm(testCtx context.Context) *keycloak.Realm {
Enabled: true,
}

err := keycloakClient.NewRealm(testCtx, r)
var err error
for i := 0; i < 3; i++ { // on CI this sometimes fails and keycloak can't be reached
err = keycloakClient.NewRealm(testCtx, r)
if err != nil {
log.Printf("Unable to create new realm: %s - retrying in 5s", err)
time.Sleep(5 * time.Second) // 24.0.5 on CI seems to have issues creating a realm when locking the table
} else {
break
}
}
if err != nil {
os.Exit(1)
log.Fatalf("Unable to create new realm: %s", err)
}

return r
Expand Down
14 changes: 14 additions & 0 deletions provider/resource_keycloak_default_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,20 @@ func resourceKeycloakDefaultRolesReconcile(ctx context.Context, data *schema.Res
return diag.FromErr(err)
}

if realm == nil {
return diag.Diagnostics{{
Severity: diag.Error,
Summary: "realm not found: " + defaultRoles.RealmId,
}}
}
if realm.DefaultRole == nil || realm.DefaultRole.Id == "" {
return diag.Diagnostics{{
Severity: diag.Error,
Summary: "realm does not have a default role",
}}

}

data.SetId(realm.DefaultRole.Id)

composites, err := keycloakClient.GetDefaultRoles(ctx, defaultRoles.RealmId, realm.DefaultRole.Id)
Expand Down
2 changes: 2 additions & 0 deletions provider/resource_keycloak_group_memberships_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func TestAccKeycloakGroupMemberships_basic(t *testing.T) {

func TestAccKeycloakGroupMemberships_basicUserWithBackslash(t *testing.T) {
t.Parallel()
// backslash usernames are weird and no longer supported >=22
skipIfVersionIsGreaterThanOrEqualTo(testCtx, t, keycloakClient, keycloak.Version_22)

groupName := acctest.RandomWithPrefix("tf-acc")
username := acctest.RandString(5) + `\\` + acctest.RandString(5)
Expand Down
Loading

0 comments on commit 3cebc1c

Please sign in to comment.