keycloak-uncached

ldap edit mode

7/31/2014 6:28:48 PM

Details

diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
index 5ad1136..587cc39 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
@@ -1,6 +1,7 @@
 package org.keycloak.federation.ldap;
 
 import org.jboss.logging.Logger;
+import org.keycloak.models.UserCredentialValueModel;
 import org.keycloak.models.UserFederationProvider;
 import org.keycloak.models.UserFederationProviderModel;
 import org.keycloak.models.KeycloakSession;
@@ -19,6 +20,7 @@ import org.picketlink.idm.model.basic.BasicModel;
 import org.picketlink.idm.model.basic.User;
 import org.picketlink.idm.query.IdentityQuery;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
@@ -35,10 +37,12 @@ public class LDAPFederationProvider implements UserFederationProvider {
     private static final Logger logger = Logger.getLogger(LDAPFederationProvider.class);
     public static final String LDAP_ID = "LDAP_ID";
     public static final String SYNC_REGISTRATIONS = "syncRegistrations";
+    public static final String EDIT_MODE = "editMode";
 
     protected KeycloakSession session;
     protected UserFederationProviderModel model;
     protected PartitionManager partitionManager;
+    protected EditMode editMode;
 
     protected static final Set<String> supportedCredentialTypes = new HashSet<String>();
 
@@ -51,6 +55,9 @@ public class LDAPFederationProvider implements UserFederationProvider {
         this.session = session;
         this.model = model;
         this.partitionManager = partitionManager;
+        String editModeString = model.getConfig().get(EDIT_MODE);
+        if (editModeString == null) editMode = EditMode.READ_ONLY;
+        editMode = EditMode.valueOf(editModeString);
     }
 
     private ModelException convertIDMException(IdentityManagementException ie) {
@@ -77,22 +84,37 @@ public class LDAPFederationProvider implements UserFederationProvider {
 
     @Override
     public UserModel proxy(UserModel local) {
-        // todo
-        return new LDAPUserModelDelegate(local, this);
+         switch (editMode) {
+             case READ_ONLY:
+                return new ReadonlyLDAPUserModelDelegate(local, this);
+             case WRITABLE:
+                return new WritableLDAPUserModelDelegate(local, this);
+             case UNSYNCED:
+                return new UnsyncedLDAPUserModelDelegate(local, this);
+         }
+        return local;
     }
 
     @Override
-    public Set<String> getSupportedCredentialTypes() {
+    public Set<String> getSupportedCredentialTypes(UserModel local) {
+        if (editMode == EditMode.UNSYNCED ) {
+            for (UserCredentialValueModel cred : local.getCredentialsDirectly()) {
+                if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
+                    return Collections.emptySet();
+                }
+            }
+        }
         return supportedCredentialTypes;
     }
 
     @Override
     public boolean synchronizeRegistrations() {
-        return "true".equalsIgnoreCase(model.getConfig().get(SYNC_REGISTRATIONS));
+        return "true".equalsIgnoreCase(model.getConfig().get(SYNC_REGISTRATIONS)) && editMode == EditMode.WRITABLE;
     }
 
     @Override
     public UserModel register(RealmModel realm, UserModel user) {
+        if (editMode == EditMode.READ_ONLY || editMode == EditMode.UNSYNCED) throw new IllegalStateException("Registration is not supported by this ldap server");;
         if (!synchronizeRegistrations()) throw new IllegalStateException("Registration is not supported by this ldap server");
         IdentityManager identityManager = getIdentityManager();
 
@@ -112,6 +134,8 @@ public class LDAPFederationProvider implements UserFederationProvider {
 
     @Override
     public boolean removeUser(RealmModel realm, UserModel user) {
+        if (editMode == EditMode.READ_ONLY || editMode == EditMode.UNSYNCED) return false;
+
         IdentityManager identityManager = getIdentityManager();
 
         try {
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/ReadonlyLDAPUserModelDelegate.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/ReadonlyLDAPUserModelDelegate.java
new file mode 100755
index 0000000..30dca4c
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/ReadonlyLDAPUserModelDelegate.java
@@ -0,0 +1,57 @@
+package org.keycloak.federation.ldap;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.UserModelDelegate;
+import org.picketlink.idm.IdentityManagementException;
+import org.picketlink.idm.IdentityManager;
+import org.picketlink.idm.credential.Password;
+import org.picketlink.idm.credential.TOTPCredential;
+import org.picketlink.idm.model.basic.BasicModel;
+import org.picketlink.idm.model.basic.User;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ReadonlyLDAPUserModelDelegate extends UserModelDelegate implements UserModel {
+    private static final Logger logger = Logger.getLogger(ReadonlyLDAPUserModelDelegate.class);
+
+    protected LDAPFederationProvider provider;
+
+    public ReadonlyLDAPUserModelDelegate(UserModel delegate, LDAPFederationProvider provider) {
+        super(delegate);
+        this.provider = provider;
+    }
+
+    @Override
+    public void setUsername(String username) {
+        throw new IllegalStateException("Federated storage is not writable");
+    }
+
+    @Override
+    public void setLastName(String lastName) {
+        throw new IllegalStateException("Federated storage is not writable");
+    }
+
+    @Override
+    public void setFirstName(String first) {
+        throw new IllegalStateException("Federated storage is not writable");
+    }
+
+    @Override
+    public void updateCredential(UserCredentialModel cred) {
+        if (provider.getSupportedCredentialTypes(delegate).contains(cred.getType())) {
+            throw new IllegalStateException("Federated storage is not writable");
+        }
+        delegate.updateCredential(cred);
+    }
+
+    @Override
+    public void setEmail(String email) {
+        throw new IllegalStateException("Federated storage is not writable");
+    }
+
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/UnsyncedLDAPUserModelDelegate.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/UnsyncedLDAPUserModelDelegate.java
new file mode 100755
index 0000000..043f8d7
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/UnsyncedLDAPUserModelDelegate.java
@@ -0,0 +1,28 @@
+package org.keycloak.federation.ldap;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.UserModelDelegate;
+import org.picketlink.idm.IdentityManagementException;
+import org.picketlink.idm.IdentityManager;
+import org.picketlink.idm.credential.Password;
+import org.picketlink.idm.credential.TOTPCredential;
+import org.picketlink.idm.model.basic.BasicModel;
+import org.picketlink.idm.model.basic.User;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class UnsyncedLDAPUserModelDelegate extends UserModelDelegate implements UserModel {
+    private static final Logger logger = Logger.getLogger(UnsyncedLDAPUserModelDelegate.class);
+
+    protected LDAPFederationProvider provider;
+
+    public UnsyncedLDAPUserModelDelegate(UserModel delegate, LDAPFederationProvider provider) {
+        super(delegate);
+        this.provider = provider;
+    }
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/WritableLDAPUserModelDelegate.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/WritableLDAPUserModelDelegate.java
new file mode 100755
index 0000000..a98d2b8
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/WritableLDAPUserModelDelegate.java
@@ -0,0 +1,132 @@
+package org.keycloak.federation.ldap;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.AuthenticationLinkModel;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserCredentialValueModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.UserModelDelegate;
+import org.picketlink.idm.IdentityManagementException;
+import org.picketlink.idm.IdentityManager;
+import org.picketlink.idm.credential.Password;
+import org.picketlink.idm.credential.TOTPCredential;
+import org.picketlink.idm.model.basic.BasicModel;
+import org.picketlink.idm.model.basic.User;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class WritableLDAPUserModelDelegate extends UserModelDelegate implements UserModel {
+    private static final Logger logger = Logger.getLogger(WritableLDAPUserModelDelegate.class);
+
+    protected LDAPFederationProvider provider;
+
+    public WritableLDAPUserModelDelegate(UserModel delegate, LDAPFederationProvider provider) {
+        super(delegate);
+        this.provider = provider;
+    }
+
+    @Override
+    public void setUsername(String username) {
+        IdentityManager identityManager = provider.getIdentityManager();
+
+        try {
+            User picketlinkUser = BasicModel.getUser(identityManager, delegate.getUsername());
+            if (picketlinkUser == null) {
+                throw new IllegalStateException("User not found in LDAP storage!");
+            }
+            picketlinkUser.setLoginName(username);
+            identityManager.update(picketlinkUser);
+        } catch (IdentityManagementException ie) {
+            throw new ModelException(ie);
+        }
+        delegate.setUsername(username);
+    }
+
+    @Override
+    public void setLastName(String lastName) {
+        IdentityManager identityManager = provider.getIdentityManager();
+
+        try {
+            User picketlinkUser = BasicModel.getUser(identityManager, delegate.getUsername());
+            if (picketlinkUser == null) {
+                throw new IllegalStateException("User not found in LDAP storage!");
+            }
+            picketlinkUser.setLastName(lastName);
+            identityManager.update(picketlinkUser);
+        } catch (IdentityManagementException ie) {
+            throw new ModelException(ie);
+        }
+        delegate.setLastName(lastName);
+    }
+
+    @Override
+    public void setFirstName(String first) {
+        IdentityManager identityManager = provider.getIdentityManager();
+
+        try {
+            User picketlinkUser = BasicModel.getUser(identityManager, delegate.getUsername());
+            if (picketlinkUser == null) {
+                throw new IllegalStateException("User not found in LDAP storage!");
+            }
+            picketlinkUser.setFirstName(first);
+            identityManager.update(picketlinkUser);
+        } catch (IdentityManagementException ie) {
+            throw new ModelException(ie);
+        }
+        delegate.setFirstName(first);
+    }
+
+    @Override
+    public void updateCredential(UserCredentialModel cred) {
+        if (!provider.getSupportedCredentialTypes(delegate).contains(cred.getType())) {
+            delegate.updateCredential(cred);
+            return;
+        }
+        IdentityManager identityManager = provider.getIdentityManager();
+
+        try {
+            User picketlinkUser = BasicModel.getUser(identityManager, getUsername());
+            if (picketlinkUser == null) {
+                logger.debugf("User '%s' doesn't exists. Skip password update", getUsername());
+                throw new IllegalStateException("User doesn't exist in LDAP storage");
+            }
+            if (cred.getType().equals(UserCredentialModel.PASSWORD)) {
+                identityManager.updateCredential(picketlinkUser, new Password(cred.getValue().toCharArray()));
+            } else if (cred.getType().equals(UserCredentialModel.TOTP)) {
+                TOTPCredential credential = new TOTPCredential(cred.getValue());
+                credential.setDevice(cred.getDevice());
+                identityManager.updateCredential(picketlinkUser, credential);
+            }
+        } catch (IdentityManagementException ie) {
+            throw new ModelException(ie);
+        }
+
+    }
+
+    @Override
+    public void setEmail(String email) {
+        IdentityManager identityManager = provider.getIdentityManager();
+
+        try {
+            User picketlinkUser = BasicModel.getUser(identityManager, delegate.getUsername());
+            if (picketlinkUser == null) {
+                throw new IllegalStateException("User not found in LDAP storage!");
+            }
+            picketlinkUser.setEmail(email);
+            identityManager.update(picketlinkUser);
+        } catch (IdentityManagementException ie) {
+            throw new ModelException(ie);
+        }
+        delegate.setEmail(email);
+    }
+
+}
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-ldap.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-ldap.html
index ffac6b7..1be350b 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-ldap.html
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/federated-ldap.html
@@ -33,6 +33,19 @@
                         <input class="form-control" id="priority" type="text" ng-model="instance.priority">
                     </div>
                 </div>
+                <div class="form-group">
+                    <label class="col-sm-2 control-label" for="editMode">Edit mode</label>
+                    <div class="col-sm-4">
+                        <div class="select-kc">
+                            <select id="editMode"
+                                    ng-model="instance.config.editMode">
+                                <option>READ_ONLY</option>
+                                <option>WRITABLE</option>
+                                <option>UNSYNCED</option>
+                            </select>
+                        </div>
+                    </div>
+                </div>
                 <div class="form-group clearfix block">
                     <label class="col-sm-2 control-label" for="syncRegistrations">Sync Registrations</label>
                     <div class="col-sm-4">
diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
index 787ca90..0e887d3 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
@@ -307,7 +307,7 @@ public class UserFederationManager implements UserProvider {
         UserFederationProvider link = getFederationLink(realm, user);
         if (link != null) {
             validateUser(realm, user);
-            if (link.getSupportedCredentialTypes().size() > 0) {
+            if (link.getSupportedCredentialTypes(user).size() > 0) {
                 List<UserCredentialModel> fedCreds = new ArrayList<UserCredentialModel>();
                 List<UserCredentialModel> localCreds = new ArrayList<UserCredentialModel>();
                 for (UserCredentialModel cred : input) {
@@ -331,7 +331,7 @@ public class UserFederationManager implements UserProvider {
         UserFederationProvider link = getFederationLink(realm, user);
         if (link != null) {
             validateUser(realm, user);
-            Set<String> supportedCredentialTypes = link.getSupportedCredentialTypes();
+            Set<String> supportedCredentialTypes = link.getSupportedCredentialTypes(user);
             if (supportedCredentialTypes.size() > 0) {
                 List<UserCredentialModel> fedCreds = new ArrayList<UserCredentialModel>();
                 List<UserCredentialModel> localCreds = new ArrayList<UserCredentialModel>();
diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java b/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java
index c45812f..c0923e4 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java
@@ -20,6 +20,28 @@ public interface UserFederationProvider extends Provider {
     public static final String LAST_NAME = UserModel.LAST_NAME;
 
     /**
+     * Optional type that can be by implementations to describe edit mode of federation storage
+     *
+     */
+    enum EditMode {
+        /**
+         * federation storage is read-only
+         */
+        READ_ONLY,
+        /**
+         * federation storage is writable
+         *
+         */
+        WRITABLE,
+        /**
+         * updates to user are stored locally and not synced with federation storage.
+         *
+         */
+        UNSYNCED
+    }
+
+
+    /**
      * Gives the provider an option to proxy UserModels loaded from local storage.
      * This method is called whenever a UserModel is pulled from local storage.
      * For example, the LDAP provider proxies the UserModel and does on-demand synchronization with
@@ -81,12 +103,12 @@ public interface UserFederationProvider extends Provider {
     boolean isValid(UserModel local);
 
     /**
-     * What UserCredentialModel types are supported by this provider.  Keycloak will only call
+     * What UserCredentialModel types should be handled by this provider for this user?  Keycloak will only call
      * validCredentials() with the credential types specified in this method.
      *
      * @return
      */
-    Set<String> getSupportedCredentialTypes();
+    Set<String> getSupportedCredentialTypes(UserModel user);
 
     /**
      * Validate credentials for this user.
@@ -98,4 +120,6 @@ public interface UserFederationProvider extends Provider {
      */
     boolean validCredentials(RealmModel realm, UserModel user, List<UserCredentialModel> input);
     boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input);
-    void close();}
+    void close();
+
+}
diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProvider.java b/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProvider.java
index 7ebf592..2559b39 100755
--- a/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProvider.java
+++ b/testsuite/integration/src/main/java/org/keycloak/testutils/DummyUserFederationProvider.java
@@ -67,7 +67,7 @@ public class DummyUserFederationProvider implements UserFederationProvider {
     }
 
     @Override
-    public Set<String> getSupportedCredentialTypes() {
+    public Set<String> getSupportedCredentialTypes(UserModel user) {
         return Collections.emptySet();
     }
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java
index 81304c8..73959d0 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java
@@ -12,6 +12,8 @@ import org.junit.runners.MethodSorters;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.federation.ldap.LDAPFederationProvider;
 import org.keycloak.federation.ldap.LDAPFederationProviderFactory;
+import org.keycloak.models.UserCredentialValueModel;
+import org.keycloak.models.UserFederationProvider;
 import org.keycloak.models.UserFederationProviderModel;
 import org.keycloak.testutils.LDAPEmbeddedServer;
 import org.keycloak.testsuite.LDAPTestUtils;
@@ -66,10 +68,11 @@ public class FederationProvidersIntegrationTest {
             String vendor = ldapServer.getVendor();
             ldapConfig.put(LDAPConstants.VENDOR, vendor);
             ldapConfig.put(LDAPFederationProvider.SYNC_REGISTRATIONS, "true");
+            ldapConfig.put(LDAPFederationProvider.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString());
 
 
 
-            ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, null);
+            ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap");
 
             // Configure LDAP
             ldapRule.getEmbeddedServer().setupLdapInRealm(appRealm);
@@ -201,4 +204,76 @@ public class FederationProvidersIntegrationTest {
         Assert.assertEquals(user.getFederationLink(), ldapModel.getId());
         keycloakRule.stopSession(session, false);
     }
+
+    @Test
+    public void testReadonly() {
+        KeycloakSession session = keycloakRule.startSession();
+        RealmModel appRealm = session.realms().getRealmByName("test");
+
+        UserFederationProviderModel model = new UserFederationProviderModel(ldapModel.getId(), ldapModel.getProviderName(), ldapModel.getConfig(), ldapModel.getPriority(), ldapModel.getDisplayName());
+        model.getConfig().put(LDAPFederationProvider.EDIT_MODE, UserFederationProvider.EditMode.READ_ONLY.toString());
+        appRealm.updateUserFederationProvider(model);
+        UserModel user = session.users().getUserByUsername("johnkeycloak", appRealm);
+        Assert.assertNotNull(user);
+        Assert.assertNotNull(user.getFederationLink());
+        Assert.assertEquals(user.getFederationLink(), ldapModel.getId());
+        try {
+            user.setEmail("error@error.com");
+            Assert.fail("should fail");
+        } catch (Exception e) {
+
+        }
+        try {
+            user.setLastName("Berk");
+            Assert.fail("should fail");
+        } catch (Exception e) {
+
+        }
+        try {
+            user.setFirstName("Bilbo");
+            Assert.fail("should fail");
+        } catch (Exception e) {
+
+        }
+        try {
+            UserCredentialModel cred = UserCredentialModel.password("poop");
+            user.updateCredential(cred);
+            Assert.fail("should fail");
+        } catch (Exception e) {
+
+        }
+        session.getTransaction().rollback();
+        session.close();
+        session = keycloakRule.startSession();
+        appRealm = session.realms().getRealmByName("test");
+        Assert.assertEquals(UserFederationProvider.EditMode.WRITABLE.toString(), appRealm.getUserFederationProviders().get(0).getConfig().get(LDAPFederationProvider.EDIT_MODE));
+        keycloakRule.stopSession(session, false);
+    }
+
+    @Test
+    public void testUnsynced() {
+        KeycloakSession session = keycloakRule.startSession();
+        RealmModel appRealm = session.realms().getRealmByName("test");
+
+        UserFederationProviderModel model = new UserFederationProviderModel(ldapModel.getId(), ldapModel.getProviderName(), ldapModel.getConfig(), ldapModel.getPriority(), ldapModel.getDisplayName());
+        model.getConfig().put(LDAPFederationProvider.EDIT_MODE, UserFederationProvider.EditMode.UNSYNCED.toString());
+        appRealm.updateUserFederationProvider(model);
+        UserModel user = session.users().getUserByUsername("johnkeycloak", appRealm);
+        Assert.assertNotNull(user);
+        Assert.assertNotNull(user.getFederationLink());
+        Assert.assertEquals(user.getFederationLink(), ldapModel.getId());
+
+        UserCredentialModel cred = UserCredentialModel.password("candy");
+        user.updateCredential(cred);
+        UserCredentialValueModel userCredentialValueModel = user.getCredentialsDirectly().get(0);
+        Assert.assertEquals(UserCredentialModel.PASSWORD, userCredentialValueModel.getType());
+        Assert.assertTrue(session.users().validCredentials(appRealm, user, cred));
+        session.getTransaction().rollback();
+        session.close();
+        session = keycloakRule.startSession();
+        appRealm = session.realms().getRealmByName("test");
+        Assert.assertEquals(UserFederationProvider.EditMode.WRITABLE.toString(), appRealm.getUserFederationProviders().get(0).getConfig().get(LDAPFederationProvider.EDIT_MODE));
+        keycloakRule.stopSession(session, false);
+    }
+
 }