Repository: epam/edp-keycloak-operator (go)

Issue:
Improve error handling for Identity Provider validation
## Summary
Added explicit identity provider existence validation before attempting to create user federated identity to prevent confusing 409 Conflict errors and provide clearer error messages.

## Problem Description

### Issue
When calling `CreateUserFederatedIdentity` with a non-existent identity provider, Keycloak returns a **409 Conflict** error. This error message is confusing and doesn't clearly indicate that the root cause is a missing identity provider.

### Impact
- **Poor Developer Experience**: 409 Conflict errors are misleading when the actual issue is a missing identity provider
- **Difficult Debugging**: Developers need to investigate further to understand the real cause
- **Inconsistent Error Handling**: Other parts of the codebase validate existence before operations

Pull request:
feat: Add validation for user identity provider
# Pull Request Template

## Description
Problem
When assigning a non-existent identity provider, Keycloak returns a 409 Conflict error. This error message is confusing and doesn't indicate that the root cause is a missing identity provider. 
Change
Added explicit identity provider existence validation before attempting to create a user federated identity to prevent confusing 409 Conflict errors and provide more explicit error messages.

Fixes #183 

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Enhancement (non-breaking change which improves an existing feature or documentation)
- [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected)

## How Has This Been Tested?
- Unit test

## Checklist:
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Pull Request contains one commit. I squash my commits.

Patch:
diff --git a/api/v1/keycloakrealmuser_types.go b/api/v1/keycloakrealmuser_types.go
index 6def78b..934012c 100644
--- a/api/v1/keycloakrealmuser_types.go
+++ b/api/v1/keycloakrealmuser_types.go
@@ -83,7 +83,7 @@ type KeycloakRealmUserSpec struct {
 	// +optional
 	PasswordSecret PasswordSecret `json:"passwordSecret,omitempty"`
 
-	// IdentityProviders linked to the user.
+	// IdentityProviders is a list of identity providers aliases linked to the user.
 	// +nullable
 	// +optional
 	IdentityProviders *[]string `json:"identityProviders,omitempty"`
diff --git a/config/crd/bases/v1.edp.epam.com_keycloakrealmusers.yaml b/config/crd/bases/v1.edp.epam.com_keycloakrealmusers.yaml
index 0fb5480..f5f6d5e 100644
--- a/config/crd/bases/v1.edp.epam.com_keycloakrealmusers.yaml
+++ b/config/crd/bases/v1.edp.epam.com_keycloakrealmusers.yaml
@@ -88,7 +88,8 @@ spec:
                 nullable: true
                 type: array
               identityProviders:
-                description: IdentityProviders linked to the user.
+                description: IdentityProviders is a list of identity providers aliases
+                  linked to the user.
                 items:
                   type: string
                 nullable: true
diff --git a/deploy-templates/_crd_examples/keycloakrealmidentityprovider.yaml b/deploy-templates/_crd_examples/keycloakrealmidentityprovider.yaml
index a66a628..a3ff1bf 100644
--- a/deploy-templates/_crd_examples/keycloakrealmidentityprovider.yaml
+++ b/deploy-templates/_crd_examples/keycloakrealmidentityprovider.yaml
@@ -5,7 +5,7 @@ metadata:
 spec:
   realmRef:
     kind: KeycloakRealm
-    name: realm
+    name: keycloakrealm-sample
   alias: instagram
   authenticateByDefault: false
   enabled: true
diff --git a/deploy-templates/crds/v1.edp.epam.com_keycloakrealmusers.yaml b/deploy-templates/crds/v1.edp.epam.com_keycloakrealmusers.yaml
index 0fb5480..f5f6d5e 100644
--- a/deploy-templates/crds/v1.edp.epam.com_keycloakrealmusers.yaml
+++ b/deploy-templates/crds/v1.edp.epam.com_keycloakrealmusers.yaml
@@ -88,7 +88,8 @@ spec:
                 nullable: true
                 type: array
               identityProviders:
-                description: IdentityProviders linked to the user.
+                description: IdentityProviders is a list of identity providers aliases
+                  linked to the user.
                 items:
                   type: string
                 nullable: true
diff --git a/docs/api.md b/docs/api.md
index 5559913..dc9e1ce 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -6154,7 +6154,7 @@ KeycloakRealmUserSpec defines the desired state of KeycloakRealmUser.
         <td><b>identityProviders</b></td>
         <td>[]string</td>
         <td>
-          IdentityProviders linked to the user.<br/>
+          IdentityProviders is a list of identity providers aliases linked to the user.<br/>
         </td>
         <td>false</td>
       </tr><tr>
diff --git a/pkg/client/keycloak/adapter/gocloak_adapter_user.go b/pkg/client/keycloak/adapter/gocloak_adapter_user.go
index c1474fe..5bf5c46 100644
--- a/pkg/client/keycloak/adapter/gocloak_adapter_user.go
+++ b/pkg/client/keycloak/adapter/gocloak_adapter_user.go
@@ -448,6 +448,15 @@ func (a GoCloakAdapter) addMissingIdentityProviders(
 			continue
 		}
 
+		exists, err := a.IdentityProviderExists(ctx, realmName, provider)
+		if err != nil {
+			return fmt.Errorf("unable to check if identity provider exists: %w", err)
+		}
+
+		if !exists {
+			return fmt.Errorf("identity provider %s does not exist", provider)
+		}
+
 		federatedIdentity := gocloak.FederatedIdentityRepresentation{
 			IdentityProvider: &provider,
 			UserID:           &userID,


Test patch:
diff --git a/pkg/client/keycloak/adapter/gocloak_adapter_user_test.go b/pkg/client/keycloak/adapter/gocloak_adapter_user_test.go
index 00b29ea..9e87c4e 100644
--- a/pkg/client/keycloak/adapter/gocloak_adapter_user_test.go
+++ b/pkg/client/keycloak/adapter/gocloak_adapter_user_test.go
@@ -32,6 +32,22 @@ func TestGoCloakAdapter_SyncRealmUser(t *testing.T) {
 			return
 		}
 
+		if strings.Contains(r.URL.Path, "identity-provider/instances/idp1") {
+			w.WriteHeader(http.StatusOK)
+			_, err := w.Write([]byte(`{"alias":"idp1"}`))
+			assert.NoError(t, err)
+
+			return
+		}
+
+		if strings.Contains(r.URL.Path, "identity-provider/instances/non-existent-idp") {
+			w.WriteHeader(http.StatusNotFound)
+			_, err := w.Write([]byte(`{"error":"idp not found"}`))
+			assert.NoError(t, err)
+
+			return
+		}
+
 		w.WriteHeader(http.StatusOK)
 	}))
 
@@ -381,6 +397,79 @@ func TestGoCloakAdapter_SyncRealmUser(t *testing.T) {
 				assert.Contains(t, err.Error(), "failed to get user")
 			},
 		},
+		{
+			name: "identity provider does not exist",
+			userDto: &KeycloakUser{
+				Username:            "user",
+				Enabled:             true,
+				EmailVerified:       true,
+				Email:               "mail@mail.com",
+				FirstName:           "first-name",
+				LastName:            "last-name",
+				RequiredUserActions: []string{"change-password"},
+				Roles:               []string{"role1"},
+				Groups:              []string{"group1"},
+				Attributes:          map[string]string{"attr1": "attr1value"},
+				Password:            "password",
+				IdentityProviders:   &[]string{"non-existent-idp"},
+			},
+			client: func(t *testing.T) *mocks.MockGoCloak {
+				m := mocks.NewMockGoCloak(t)
+
+				m.On("GetUsers", mock.Anything, "", "realm", mock.Anything).
+					Return(nil, nil)
+				m.On("CreateUser",
+					mock.Anything,
+					"",
+					"realm",
+					mock.MatchedBy(func(user gocloak.User) bool {
+						return assert.Equal(t, "user", *user.Username)
+					})).
+					Return("user-id", nil)
+				m.On("GetRoleMappingByUserID", mock.Anything, "", "realm", "user-id").
+					Return(&gocloak.MappingsRepresentation{
+						RealmMappings:  &[]gocloak.Role{},
+						ClientMappings: map[string]*gocloak.ClientMappingsRepresentation{},
+					}, nil)
+				m.On("GetRealmRole", mock.Anything, "", "realm", "role1").
+					Return(&gocloak.Role{
+						Name: gocloak.StringP("role1"),
+						ID:   gocloak.StringP("role1-id"),
+					}, nil)
+				m.On("AddRealmRoleToUser",
+					mock.Anything,
+					"",
+					"realm",
+					"user-id",
+					mock.MatchedBy(func(roles []gocloak.Role) bool {
+						return assert.Len(t, roles, 1) &&
+							assert.Equal(t, "role1-id", *roles[0].ID)
+					})).
+					Return(nil)
+				m.On("GetGroups",
+					mock.Anything,
+					"",
+					"realm",
+					mock.Anything).
+					Return([]*gocloak.Group{{
+						Name: gocloak.StringP("group1"),
+						ID:   gocloak.StringP("group1-id"),
+					}}, nil)
+				m.On("RestyClient").Return(resty.New())
+				m.On("GetUserFederatedIdentities",
+					mock.Anything,
+					"",
+					"realm",
+					"user-id").
+					Return([]*gocloak.FederatedIdentityRepresentation{}, nil)
+
+				return m
+			},
+			wantErr: func(t require.TestingT, err error, i ...interface{}) {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), "identity provider non-existent-idp does not exist")
+			},
+		},
 	}
 
 	for _, tt := range tests {