adorsys / keycloak-config-cli

Import YAML/JSON-formatted configuration files into Keycloak - Configuration as Code for Keycloak.
Apache License 2.0
704 stars 132 forks source link

LDAP federated user update fails with 400 Bad Request #496

Closed mpreu closed 2 years ago

mpreu commented 2 years ago

Describe the bug Having a Keycloak configured with a LDAP federation I can initially import users and modify e.g. their groups just fine. I also can run the configuration again for some time to change e.g. user groups again. At some point, I get a 400 Bad Request when running the same configurations again (with or without changes).

Keycloak then fails with:

[org.keycloak.userprofile.validator.ReadOnlyAttributeUnchangedValidator] (default task-57) Attempt to edit denied attribute '(?i:^\QKERBEROS_PRINCIPAL\E$|^\QLDAP_ID\E$|^\QLDAP_ENTRY_DN\E$|^\QCREATED_TIMESTAMP\E$|^\QcreateTimestamp\E$|^\QmodifyTimestamp\E$)' of user 'user1'

Deleting the users from KeyCloak removes the error, but it appears again after some invocations of the config.

I cannot find a clear connection to e.g. the number of invocations or the time past after the initial user import. But my current assumption is, that the error occurs after a certain time past the initial user import.

EDIT: I can verify, that the error does not happen every time after its first occurrence. With a completely unchanged failing config I was able to rerun it without failure once after some hours. Next invocation failed again then though.

To Reproduce

  1. Create Keycloak with LDAP federation in READ_ONLY mode
  2. Use yaml configuration to import a federated user and assign a group to it
    realm: test
    users:
        - username: user1
          federationLink: internal-ldap
          groups:
             - group1
  3. Change the group assignment and expect it to succeed
     realm: test
     users:
        - username: user1
          federationLink: internal-ldap
          groups:
             - group2
  4. After some time running the configurations from above again fails with the mentioned error
  5. Delete user1 in Keycloak, repeat from step 2. and observe the configuration to succeed again for some time

Expected behavior To be able to repeat configurations for LDAP federated users without the try to modify immutable metadata (according to the Keycloak error message).

Environment (please complete the following information):

jkroepke commented 2 years ago

can you provide a full realm?

If I create a provider with READ_ONLY, i'm not able to add groups to the user.

22:03:22,756 ERROR [org.keycloak.services.error.KeycloakErrorHandler] (default task-11) Uncaught server error: org.keycloak.models.ModelException: Not possible to write 'group mapping for group group1' when updating user 'jbrown'

I'm using this config:

https://github.com/adorsys/keycloak-config-cli/blob/21cbf42f2aa6c584dbbd57509147bf1d00e6a889/src/test/resources/import-files/user-federation/04_update_realm_with_federation_readonly_add_group.json

mpreu commented 2 years ago

Hi,

my minimal configuration (with some blacked-out LDAP values) looks like this:

realm: test
id: test
displayName: Test Realm
enabled: true
verifyEmail: false
registrationAllowed: false
registrationEmailAsUsername: false
loginWithEmailAllowed: false
duplicateEmailsAllowed: false
resetPasswordAllowed: false
editUsernameAllowed: false
components:
  org.keycloak.storage.UserStorageProvider:
    - name: internal-ldap
      id: internal-ldap
      providerId: ldap
      config:
        vendor:
          - ad
        editMode: 
          - READ_ONLY
        usernameLDAPAttribute: 
          - sAMAccountName
        rdnLDAPAttribute: 
          - cn
        uuidLDAPAttribute: 
          - objectGUID
        userObjectClasses: 
          - "person, organizationalPerson, user, top"
        connectionUrl: 
          - "ldap://xxx:3268"
        usersDn: 
          - "dc=xx"
        customUserSearchFilter: 
          - "(sAMAccountName=*)"
        searchScope: 
          - "2" # sub
        authType: 
          - simple
        bindDn: 
          - "CN=xxx,OU=1234,OU=xxx,OU=Domain Users,DC=xx,DC=xx"
        # This reads the REALM-SECRET from /opt/jboss/keycloak/secrets
        bindCredential: 
          - "${vault.bindCredentialLDAP}"
        importEnabled:
          - "true"
        batchSizeForSync: 
          - "1000"
        changedSyncPeriod:
          - "86400"
      subComponents:
        org.keycloak.storage.ldap.mappers.LDAPStorageMapper:
          - name: username
            providerId: user-attribute-ldap-mapper
            config:
              ldap.attribute:
                - sAMAccountName
              user.model.attribute:
                - username
              is.mandatory.in.ldap:
                - "true"
              is.binary.attribute:
                - "false"
              always.read.value.from.ldap:
                - "false"
              read.only:
                - "true"
          - name: first name
            providerId: user-attribute-ldap-mapper
            config:
              ldap.attribute:
                - givenName
              user.model.attribute:
                - firstName
              is.mandatory.in.ldap:
                - "false"
              is.binary.attribute:
                - "false"
              always.read.value.from.ldap:
                - "false"
              read.only:
                - "true"
          - name: last name
            providerId: user-attribute-ldap-mapper
            config:
              ldap.attribute:
                - sn
              user.model.attribute:
                - lastName
              is.mandatory.in.ldap:
                - "true"
              is.binary.attribute:
                - "false"
              always.read.value.from.ldap:
                - "false"
              read.only:
                - "true"
          - name: full name
            providerId: full-name-ldap-mapper
            config:
              ldap.full.name.attribute:
                - cn
              read.only:
                - "true"
              write.only:
                - "false"
          - name: email
            providerId: user-attribute-ldap-mapper
            config:
              ldap.attribute:
                - mail
              user.model.attribute:
                - email
              is.mandatory.in.ldap:
                - "false"
              is.binary.attribute:
                - "false"
              always.read.value.from.ldap:
                - "false"
              read.only:
                - "true"
groups:
  - name: realm
    path: "/realm"
    subGroups:
      - name: admin
        path: "/realm/admin"
        clientRoles:
          realm-management:
            - realm-admin
users:
  - username: user1
    federationLink: internal-ldap
    groups:
      - /realm/admin

results in a correct user representation in Keycloak:

[ {
    "id" : "x",
    "createdTimestamp" : "x",
    "username" : "user1",
    "enabled" : true,
    "totp" : false,
    "emailVerified" : false,
    "firstName" : "x",
    "lastName" : "x",
    "email" : "x",
    "federationLink" : "internal-ldap",
    "attributes" : {
      "LDAP_ENTRY_DN" : [ "CN=x,OU=x,OU=x,OU=Domain Users,DC=x,DC=x" ],
      "modifyTimestamp" : [ "x" ],
      "createTimestamp" : [ "x" ],
      "LDAP_ID" : [ "CORRESPONDING_LDAP_ID" ]
    },
    "disableableCredentialTypes" : [ ],
    "requiredActions" : [ ],
    "notBefore" : 0,
    "access" : {
      "manageGroupMembership" : true,
      "view" : true,
      "mapRoles" : true,
      "impersonate" : true,
      "manage" : true
    }
  } ]

I cannot remember to have experienced an error similar to yours. Getting the config to run was rather easy when I figured out how the specify the federationLink.

jkroepke commented 2 years ago

Thanks.

Using a full qualified group name

    groups:
      - /realm/admin

works now.

While looking deeper into it, I saw that keycloak-config-cli creates a user first, then it will create the user storage provider. I will swap that.

jkroepke commented 2 years ago

Hey @mpreu

i have some questions:

Did you know, if there are multiple hosts behind "ldap://xxx:3268" (I guess yes in a typical AD setup)? Are you able reproduce the error, if you use the address of one domain controller directly?

Are you able to reproduce the error, if you import the user after configure the ldap provider? (like in multiple runs) + avoid define federationLink: internal-ldap

Looking at source here, the exception:

[org.keycloak.userprofile.validator.ReadOnlyAttributeUnchangedValidator] (default task-57) Attempt to edit denied attribute '(?i:^\QKERBEROS_PRINCIPAL\E$|^\QLDAP_ID\E$|^\QLDAP_ENTRY_DN\E$|^\QCREATED_TIMESTAMP\E$|^\QcreateTimestamp\E$|^\QmodifyTimestamp\E$)' of user 'user1'

is raised, if the value of user attributes has been changed.

user attributes are not only used for metadata, the user attributes are also used for additional user profile data.

Setting user attributes is a valid case. By default, keycloak-config-cli will set the attributed from existing user, if no attributes are defined inside the import:

https://github.com/adorsys/keycloak-config-cli/blob/4261010b4cb1fbdd3578251955eaf405683e27d8/src/main/java/de/adorsys/keycloak/config/service/UserImportService.java#L143-L145

In any case, the same values from Keycloak are used and send back to the Keycloak itself.

Normally, it's not possible that this error accours unless the value has been change at keycloak side between GET and POST request.

Looks at the attributes

    "attributes" : {
      "LDAP_ENTRY_DN" : [ "CN=x,OU=x,OU=x,OU=Domain Users,DC=x,DC=x" ],
      "modifyTimestamp" : [ "x" ],
      "createTimestamp" : [ "x" ],
      "LDAP_ID" : [ "CORRESPONDING_LDAP_ID" ]
    },

it could be possible, if there are multiple hosts behind "ldap://xxx:3268", each AD instance may have different values of createTimestamp or modifyTimestamp or a replication lag could be the reason here.

You are describe the error occurs rarely, this may only happens if multiple hosts having different values for modifyTimestamp and the ldap backend for get user1 and update user1 request was different, then this error may occurs.

mpreu commented 2 years ago

It should be a regular AD setup and it is very likely that there are multiple hosts behind this configured LDAP (considering the information I have). But I have to admit, I do not have deeper knowledge about the general setup in this specific case. So verifying against a single Domain Controller is not possible for me right now.

I can verify though, that after the initial import of a federated user and then going forward with configurations not having the federationLink

users:
  - username: user1
    groups:
      - /realm/admin

will fail at some point as well. Btw. the time frame where I expect the error to happen is rather short (max. 5-10min).

Updates in the frontend are not showing similar errors, since the calls made for this are just targeting the groups specifically from what I see in the request.

But what I noticed helps in general if the error occurs, is to change the group settings of the user over the frontend (e.g. deleting the groups). After that the configuration above works fine again until the error occurs again.

Would it be possible to filter read_only attributes and send them with empty values during the update? According to https://issues.redhat.com/browse/KEYCLOAK-18922 and https://issues.redhat.com/browse/KEYCLOAK-18916 Keycloak will not fail the validation then.

jkroepke commented 2 years ago

will fail at some point as well.

If you are using master or latest stable release, I would expect that. Since the user is created before the federationStorage is configured which is limited? supported. In the next release, the federationStorage is configured first, then the handling of user secondly.

I would expect this issue too, if plain curl is used to update the user including the attributes in the update request).

Anyway, to resolve this specific case, I introduce a flag import.skip-attributes-for-federated-user #497 that forcefully set attributes=null before importing users to Keycloak. If set, user attributes are not longer manageable through keycloak-config-cli.

mpreu commented 2 years ago

@jkroepke Thanks, will try it out. For my use cases it would be currently ok to have such a specific flag.

But I just wanted to confirm, that setting the modifyTimestamp mapper to read.always.from.ldap=false seems to help with this issue (at least during the last 2 hours I could not reproduce the error). Not sure if that is undesirable for the most part to deviate from the default true setting.

Might be helpful if that confirms to be true in the long run for others having to deal with custom attributes.