[keycloak-dev] Transactions WAS Re: User Federation Provider Cache
Bill Burke
bburke at redhat.com
Fri Jun 17 09:45:08 EDT 2016
On 6/17/16 4:59 AM, Marek Posolda wrote:
> On 16/06/16 16:38, Bill Burke wrote:
>>>>> Transactions and 3rd party updates
>>>>> -----------------------------------------------
>>>>>
>>>>> - Will be good to improve registration of user to LDAP. Ideally during
>>>>> registration new user to LDAP, we should allow to send all data at
>>>>> once.
>>>>> (currently UserFederationProvider.register supports sending just
>>>>> username). Also we should allow to specify if register to 3rd party
>>>>> provider should be done *before* or *after* the registration to
>>>>> Keycloak
>>>>> local DB. For details, see
>>>>> https://issues.jboss.org/browse/KEYCLOAK-1075
>>>>> and all the comments from users...
>>>>>
>>>>> - Also updating user should be ideally at once. For example if you
>>>>> call:
>>>>> user.setFirstName("john");
>>>>> user.setLastName("Doe");
>>>>> user.setEmail("john at email.cz");
>>>>>
>>>>> we shouldn't have 3 update calls to LDAP, but just one. Maybe we can
>>>>> address this with transaction abstraction? I've already did something
>>>>> for LDAP provider (see TxAwareLDAPUserModelDelegate ), however will be
>>>>> good to provide something more generic for user storage SPI. Then when
>>>>> KeycloakTransactionManager.commit is called, the data are send to
>>>>> federation storage
>>>>>
>>>>
>>>> This would be an implementation detail. Similar to JPA Users, the
>>>> LDAP UserAdapter will be remembered per session. The LDAP adapter
>>>> could queue up updates then at commit time flush them all.
>>>>
>>>> Maybe you should consider writing an LDAP version of JPA? ;-)
>>>> Probably a lot of code could be borrowed from PL IDM API.
>>> I've wrote already something similar for Mongo ;) Also I already
>>> addressed the multiple-updates issue for LDAP (see above). However this
>>> one is mainly issue for 3rd party providers rather than for LDAP
>>> provider. Especially since 3rd party provider may have some validations
>>> for some attributes.
>>>
>>>
>>> For example consider this scenario, which can happen in current
>>> implementation:
>>>
>>> UserModel john = session.users().addUser(realm, "john");
>>> john.setEmail("john at email.org");
>>>
>>> All of this happens during "addUser" call:
>>>
>>> - User "john" is saved to local-db
>>> - SomeUserFederationProvider.register calls the registration request to
>>> 3rd party federation storage. User "john" is saved in 3rd party storage
>>> now.
>>>
>>> Then john.setEmail is invoked
>>> - There is request to 3rd party federation storage to update email.
>>> - 3rd party storage refuse to save the email because there is already an
>>> existing user with email "john at email.org" . So it sends back the
>>> validation error. Now keycloak can rollback the transaction, however
>>> user "john" was already registered in 3rd party federation storage from
>>> previous "register" call -> FAIL
>>>
>>> So now you need to manually remove the user from 3rd party storage,
>>> which is dirty hack. There are other possible hacks to address this. For
>>> example someone "postpone" the registration in "register" method by
>>> adding some helper attributes and sends the registration request later
>>> when all the attributes are set. I suggest to read the comments in
>>> https://issues.jboss.org/browse/KEYCLOAK-1075 for details.
>>>
>>> IMO the proper solution is to ensure that "register" request to 3rd
>>> party is postponed until the transaction.commit when user is filled with
>>> all the attributes. Similarly update should be done just once. IMO we
>>> should provide something at SPI level to simplify this scenario.
>>>
>>
>> We already have a KeycloakTransaction you can register with.
>>
>>> I understand that main motivation for you is to avoid "imports", however
>>> since we are refactoring anyway, we should try to address other SPI
>>> limitations too. And IMO this one is pretty bad and worth improve ;)
>>
>> So, have a method addUser(RealmModel realm, UserEntity user)?
> I am thinking about adding some helper classes, so the people don't need
> to directly deal with our transaction API and manually enlist
> transactions in their code etc.
>
> Also it will be good to have some support for validations (aka. pseudo
> 2-phase commit), so that all "small" userStores have possibility to
> first validate if created/updated user is valid for them and then the
> real update is send. This will help to avoid situation like:
> - transaction.commit is invoked
> - store1 successfully register the user and commits
> - store2 rejects to register user because of some validation error (ie.
> duplicated email)
> - now some data of invalid user already saved in store1 -> FAIL .
> Because at this point, whole transaction should be rollback and user
> shouldn't be saved anywhere.
>
>
> I am thinking about pseudo-code similar to this:
>
> public abstract class FederationTransaction implements KeycloakTransaction {
>
> protected TransactionState state = TransactionState.NOT_STARTED;
> protected final TxUserStore userStore;
> protected final TxUserModel user;
>
> public FederationTransaction(TxUserStore userStore, TxUserModel user) {
> this.userStore = userStore;
> this.user = user;
> }
>
> // Implement common stuff like begin, rollback, setRollbackOnly...
>
> }
>
>
> // This transaction just validates in 3rd party store if user is ok
> public class ValidationTransaction extends FederationTransaction {
>
> public ValidationTransaction(TxUserStore userStore, TxUserModel user) {
> super(userStore, user);
> }
>
> @Override
> protected void commit() {
> userStore.validateUser(user);
> }
>
> }
>
>
> // This transaction finally sends commit to 3rd party store
> public class CommitTransaction extends FederationTransaction {
>
> public CommitTransaction(TxUserStore userStore, TxUserModel user) {
> super(userStore, user);
> }
>
> @Override
> protected void commit() {
> userStore.createOrUpdateUser(user);
> }
>
> }
>
>
> public abstract class TxUserStore implements UserStore {
>
> private final KeycloakSession session;
>
> public TxUserStore(KeycloakSession session) {
> this.session = session;
> }
>
> public KeycloakSession getSession() {
> return session;
> }
>
> // Send request to storage to validate user
> protected abstract void validateUser(TxUserModel user) throws
> FederationValidationException;
>
> // Send request to create or update user during transaction.commit
> when all stores successfully validated user
> protected abstract void createOrUpdateUser(TxUserModel user);
> }
>
>
>
> public class TxUserModel extends UserModelDelegate {
>
> private ValidationTransaction validationTransaction;
> private CommitTransaction commitTransaction;
>
> private final TxUserStore store;
>
> public TxUserModel(UserModel delegate, TxUserStore store) {
> super(delegate);
> this.store = store;
> }
>
>
> protected void ensureTransactionEnlisted() {
> if (commitTransaction == null) {
> validationTransaction = new ValidationTransaction(store, this);
> commitTransaction = new CommitTransaction(store, this);
>
> // Ensure all validation transactions are called before
> sending real "commit" to any storage
>
> store.getSession().getTransaction().enlistPrepare(validationTransaction);
>
> // Enlisting real transaction here
>
> store.getSession().getTransaction().enlistAfterCompletion(commitTransaction);
> }
> }
>
> }
>
>
>
> Now in your real store implementation, you need to do just this to
> ensure that registration of user is postponed to transaction commit:
>
> public UserModel register(UserModel register(RealmModel realm, UserModel
> user) {
> MyTxUserModel wrappedUser = new MyTxUserModel(user, this);
> wrappedUser.ensureTransactionEnlisted();
> }
>
>
> And also ensure that transaction is enlisted during any update calls. So
> in MyTxUserModel override the needed setter methods like this:
>
> @Override
> public void setFirstName(String firstName) {
> ensureTransactionEnlisted();
> super.setFirstName(firstName);
> }
>
> @Override
> public void setLastName(String lastName) {
> ensureTransactionEnlisted();
> super.setLastName(lastName);
> }
>
>
> With this approach, when you have:
> UserModel john = session.users().addUser("john", realm);
> john.setFirstName("John");
> john.setLastName("Doe");
>
> you have the register call postponed until the transaction.commit .
>
> The thing is, that with postponed registration you won't know the ID of
> newly created user at the moment when "addUser" is called. However not
> sure if that's big issue... Another approach might be that UserStore
> will have possibility to specify if it supports transactions or not.
> Then we can have method "rollback" on UserStore, which will be called if
> keycloakTransaction.rollback is called.
>
Isn't adding:
addUser(org.keycloak.models.entities.UserEntity)
much simpler than all of the things you propose? We change the
registration flow to build up a UserEntity object that is eventually
used to create the user.
Bill
More information about the keycloak-dev
mailing list