This is an automated email from the ASF dual-hosted git repository.

angela pushed a commit to branch OAK-10517
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git

commit 413ea5908c3a35dcb8e45c2b589dd290c9c7eee6
Author: angela <[email protected]>
AuthorDate: Thu Oct 26 18:36:07 2023 +0200

    OAK-10517 : Consistently clean membership when switch between default and 
dynamic sync
---
 .../external/basic/DefaultSyncContext.java         |   9 ++
 .../external/impl/DynamicSyncContext.java          |  11 +-
 .../external/impl/ExternalIdentityConstants.java   |   9 ++
 .../external/impl/SwitchSyncModeTest.java          | 160 +++++++++++++++++++++
 4 files changed, 188 insertions(+), 1 deletion(-)

diff --git 
a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncContext.java
 
b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncContext.java
index bf4ba40c04..1786fe39f1 100644
--- 
a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncContext.java
+++ 
b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncContext.java
@@ -60,6 +60,8 @@ import org.slf4j.LoggerFactory;
 
 import static java.text.Normalizer.Form.NFKC;
 import static java.text.Normalizer.normalize;
+import static 
org.apache.jackrabbit.oak.spi.security.authentication.external.impl.ExternalIdentityConstants.REP_EXTERNAL_PRINCIPAL_NAMES;
+import static 
org.apache.jackrabbit.oak.spi.security.authentication.external.impl.ExternalIdentityConstants.REP_LAST_DYNAMIC_SYNC;
 
 /**
  * Internal implementation of the sync context
@@ -588,6 +590,13 @@ public class DefaultSyncContext implements SyncContext {
             log.debug("- removing member '{}' for group '{}'", auth.getID(), 
grp.getID());
         }
         timer.mark("removing");
+        
+        // make sure properties added by 'dynamic sync' are cleared
+        if (!auth.isGroup()) {
+            auth.removeProperty(REP_EXTERNAL_PRINCIPAL_NAMES);
+            auth.removeProperty(REP_LAST_DYNAMIC_SYNC);
+            timer.mark("cleanup");
+        }
         log.debug("syncMembership({}) {}", external.getId(), timer);
     }
     
diff --git 
a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DynamicSyncContext.java
 
b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DynamicSyncContext.java
index fb331ff0b6..18a24ea334 100644
--- 
a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DynamicSyncContext.java
+++ 
b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DynamicSyncContext.java
@@ -156,6 +156,10 @@ public class DynamicSyncContext extends DefaultSyncContext 
{
             super.syncMembership(external, auth, depth);
         } else {
             try {
+                // determine if clean up of groups (i.e. getting rid of 
previously synchronized membership information)
+                // is required or not. due to OAK-10517 just checking 
'groupsSyncedBefore' is not sufficient.
+                boolean cleanupGroups = groupsSyncedBefore || 
requiresCleanup(auth);
+                
                 Iterable<ExternalIdentityRef> declaredGroupRefs = 
external.getDeclaredGroups();
                 // resolve group-refs respecting depth to avoid iterating twice
                 Map<ExternalIdentityRef, SyncEntry> map = 
collectSyncEntries(declaredGroupRefs, depth);
@@ -170,7 +174,7 @@ public class DynamicSyncContext extends DefaultSyncContext {
                 }
                 
                 // clean up any other membership
-                if (groupsSyncedBefore) {
+                if (cleanupGroups) {
                     clearGroupMembership(auth);
                 }
             } catch (ExternalIdentityException e) {
@@ -201,6 +205,7 @@ public class DynamicSyncContext extends DefaultSyncContext {
             vs = createValues(principalsNames);
         }
         
authorizable.setProperty(ExternalIdentityConstants.REP_EXTERNAL_PRINCIPAL_NAMES,
 vs);
+        
authorizable.setProperty(ExternalIdentityConstants.REP_LAST_DYNAMIC_SYNC, 
nowValue);
     }
     
     @NotNull
@@ -378,6 +383,10 @@ public class DynamicSyncContext extends DefaultSyncContext 
{
     private static boolean groupsSyncedBefore(@NotNull Authorizable 
authorizable) throws RepositoryException {
         return authorizable.hasProperty(REP_LAST_SYNCED) && 
!authorizable.hasProperty(ExternalIdentityConstants.REP_EXTERNAL_PRINCIPAL_NAMES);
     }
+    
+    private static boolean requiresCleanup(@NotNull Authorizable authorizable) 
throws RepositoryException {
+        return authorizable.hasProperty(REP_LAST_SYNCED) && 
!authorizable.hasProperty(ExternalIdentityConstants.REP_LAST_DYNAMIC_SYNC);
+    }
 
     private static boolean isEveryone(@NotNull Group group) {
         try {
diff --git 
a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalIdentityConstants.java
 
b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalIdentityConstants.java
index d4fcf4007f..b55e73293a 100644
--- 
a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalIdentityConstants.java
+++ 
b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalIdentityConstants.java
@@ -62,6 +62,15 @@ public interface ExternalIdentityConstants {
      */
     String REP_EXTERNAL_PRINCIPAL_NAMES = "rep:externalPrincipalNames";
 
+    /**
+     * Name of the property storing the date of the last synchronization of 
the dynamic membership of an 
+     * external user together with {@link #REP_EXTERNAL_PRINCIPAL_NAMES}.
+     * This property is of type {@link 
org.apache.jackrabbit.oak.api.Type#DATE}.
+     * 
+     * @see <a 
href="https://issues.apache.org/jira/browse/OAK-10517";>OAK-10517</a>
+     */
+    String REP_LAST_DYNAMIC_SYNC = "rep:lastDynamicSync";
+
     /**
      * The set of served property names defined by this interface.
      */
diff --git 
a/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SwitchSyncModeTest.java
 
b/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SwitchSyncModeTest.java
new file mode 100644
index 0000000000..4ad3c123e5
--- /dev/null
+++ 
b/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SwitchSyncModeTest.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.oak.spi.security.authentication.external.impl;
+
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+import 
org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;
+import 
org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalUser;
+import 
org.apache.jackrabbit.oak.spi.security.authentication.external.SyncResult;
+import 
org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncConfig;
+import 
org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncContext;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import javax.jcr.Value;
+import javax.jcr.ValueFactory;
+
+import java.util.Calendar;
+
+import static 
org.apache.jackrabbit.oak.spi.security.authentication.external.impl.ExternalIdentityConstants.REP_EXTERNAL_PRINCIPAL_NAMES;
+import static 
org.apache.jackrabbit.oak.spi.security.authentication.external.impl.ExternalIdentityConstants.REP_LAST_DYNAMIC_SYNC;
+import static 
org.apache.jackrabbit.oak.spi.security.authentication.external.impl.ExternalIdentityConstants.REP_LAST_SYNCED;
+import static 
org.apache.jackrabbit.oak.spi.security.user.UserConstants.REP_MEMBERS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public class SwitchSyncModeTest extends AbstractDynamicTest {
+
+    @NotNull ExternalUser syncPriorToDynamicMembership() throws Exception {
+        return syncWithDefaultContext(SyncResult.Status.ADD);
+    }
+    
+    private @NotNull ExternalUser syncWithDefaultContext(@NotNull 
SyncResult.Status expectedStatus) throws Exception {
+        DefaultSyncContext defaultSyncContext = createDefaultSyncContext(r);
+        ExternalUser previouslySyncedUser = idp.getUser(USER_ID);
+        assertNotNull(previouslySyncedUser);
+
+        SyncResult result = defaultSyncContext.sync(previouslySyncedUser);
+        assertSame(expectedStatus, result.getStatus());
+        defaultSyncContext.close();
+        r.commit();
+
+        return previouslySyncedUser;
+    }
+    
+    @Override
+    protected @NotNull DefaultSyncConfig createSyncConfig() {
+        DefaultSyncConfig config = super.createSyncConfig();
+        config.user().setDynamicMembership(true)
+                .setEnforceDynamicMembership(true)
+                .setMembershipExpirationTime(1)
+                .setExpirationTime(1);
+        config.group().setDynamicGroups(true)
+                .setExpirationTime(1);
+        return config;
+    }
+    
+    private @NotNull DefaultSyncContext createDefaultSyncContext(@NotNull Root 
r) {
+        UserManager um = getUserManager(r);
+        return new DefaultSyncContext(createNonDynamicConfig(), idp, um, 
getValueFactory(r));
+    }
+    
+    private @NotNull DefaultSyncConfig createNonDynamicConfig() {
+        DefaultSyncConfig config = createSyncConfig();
+        config.user().setDynamicMembership(false)
+                .setEnforceDynamicMembership(false)
+                .setMembershipNestingDepth(Long.MAX_VALUE)
+                .setMembershipExpirationTime(1);
+        config.group().setDynamicGroups(false);
+        return config;
+    }
+    
+    private static void assertIsDynamic(@NotNull ExternalUser externalUser, 
@NotNull UserManager um, @NotNull Root root, boolean expectedDynamic) throws 
Exception {
+        User user = um.getAuthorizable(externalUser.getId(), User.class);
+        assertNotNull(user);
+        assertEquals(expectedDynamic, 
user.hasProperty(REP_EXTERNAL_PRINCIPAL_NAMES));
+        assertEquals(expectedDynamic, user.hasProperty(REP_LAST_DYNAMIC_SYNC));
+        assertTrue(user.hasProperty(REP_LAST_SYNCED));
+        
+        assertGroups(externalUser, um, root, !expectedDynamic);
+    }
+    
+    private static void assertGroups(@NotNull ExternalUser externalUser, 
@NotNull UserManager um, @NotNull Root r,  boolean expectedMemberReference) 
throws Exception {
+        for (ExternalIdentityRef groupRef : externalUser.getDeclaredGroups()) {
+            Group gr = um.getAuthorizable(groupRef.getId(), Group.class);
+            assertNotNull(gr);
+            
+            Tree t = r.getTree(gr.getPath());
+            assertEquals(expectedMemberReference, t.hasProperty(REP_MEMBERS));
+        }
+    }
+
+    @Test
+    public void testDefaultDynamic() throws Exception {
+        ExternalUser externalUser = idp.getUser(USER_ID);
+        assertNotNull(externalUser);
+        
+        // assert previously synched user is not dynamic
+        assertIsDynamic(externalUser, getUserManager(r), r, false);
+
+        // assert previously synched user is now dynamic
+        sync(externalUser, SyncResult.Status.UPDATE);
+        assertIsDynamic(externalUser, getUserManager(r), r, true);
+    }
+
+    @Test
+    public void testDefaultMissingCleanupDynamic() throws Exception {
+        ExternalUser externalUser = idp.getUser(USER_ID);
+        assertNotNull(externalUser);
+        
+        // modified the default-synchronized user to mock the issue reported 
in OAK-10517 
+        UserManager um = getUserManager(r);
+        User user = um.getAuthorizable(externalUser.getId(), User.class);
+        user.setProperty(REP_EXTERNAL_PRINCIPAL_NAMES, new Value[] 
{getValueFactory(r).createValue("test-values")});
+        r.commit();
+        
+        // assert previously synched user is now dynamic and groups have been 
cleaned
+        sync(externalUser, SyncResult.Status.UPDATE);
+        assertIsDynamic(externalUser, getUserManager(r), r, true);
+    }
+
+    @Test
+    public void testDefaultSyncClearsDynamicProperties() throws Exception {
+        ExternalUser externalUser = idp.getUser(USER_ID);
+        assertNotNull(externalUser);
+
+        assertIsDynamic(externalUser, getUserManager(r), r, false);
+
+        // modified the default-synchronized user to mock the issue reported 
in OAK-10517 
+        UserManager um = getUserManager(r);
+        User user = um.getAuthorizable(externalUser.getId(), User.class);
+        ValueFactory vf = getValueFactory(r);
+        user.setProperty(REP_EXTERNAL_PRINCIPAL_NAMES, new Value[] 
{vf.createValue("test-values")});
+        user.setProperty(REP_LAST_DYNAMIC_SYNC, 
vf.createValue(Calendar.getInstance()));
+        r.commit();
+
+        // assert previously synched user is now dynamic and groups have been 
cleaned
+        syncWithDefaultContext(SyncResult.Status.UPDATE);
+        assertIsDynamic(externalUser, getUserManager(r), r, false);
+    }
+}
\ No newline at end of file

Reply via email to