Index: src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/jmx/Delegatee.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/jmx/Delegatee.java (revision 763a737254cde4c1a8ff9d59034089758c5e7fdc) +++ src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/jmx/Delegatee.java (revision ) @@ -391,6 +391,8 @@ case ADD: case DELETE: case UPDATE: + case ENABLE: + case DISABLE: append(list, result.getIdentity(), e); break; default: @@ -414,6 +416,12 @@ break; case DELETE: op = "del"; + break; + case ENABLE: + op = "ena"; + break; + case DISABLE: + op = "dis"; break; case NO_SUCH_AUTHORIZABLE: op = "nsa"; Index: src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/package-info.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/package-info.java (revision 763a737254cde4c1a8ff9d59034089758c5e7fdc) +++ src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/package-info.java (revision ) @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@Version("2.0.0") +@Version("2.1.0") @Export package org.apache.jackrabbit.oak.spi.security.authentication.external; \ No newline at end of file Index: src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncConfig.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncConfig.java (revision 763a737254cde4c1a8ff9d59034089758c5e7fdc) +++ src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncConfig.java (revision ) @@ -229,6 +229,8 @@ private boolean dynamicMembership; + private boolean disableMissing; + /** * Returns the duration in milliseconds until the group membership of a user is expired. If the * membership information is expired it is re-synced according to the maximum nesting depth. @@ -310,6 +312,22 @@ return this; } + /** + * Controls the behavior for users that no longer exist on the external provider. The default is to delete the repository users + * if they no longer exist on the external provider. If set to true, they will be disabled instead, and re-enabled once they appear + * again. + */ + public boolean getDisableMissing() { + return disableMissing; + } + + /** + * @see #getDisableMissing() + */ + public User setDisableMissing(boolean disableMissing) { + this.disableMissing = disableMissing; + return this; + } } /** \ No newline at end of file Index: src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncContextTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncContextTest.java (revision 763a737254cde4c1a8ff9d59034089758c5e7fdc) +++ src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncContextTest.java (revision ) @@ -386,6 +386,87 @@ } @Test + public void testSyncDisabledUserById() throws Exception { + // configure to disable missing users + syncConfig.user().setDisableMissing(true); + syncCtx.setForceUserSync(true); + syncConfig.user().setMembershipNestingDepth(1); + + // sync user normally first + TestIdentityProvider.TestUser user = (TestIdentityProvider.TestUser) idp.listUsers().next(); + assertNotNull(user); + String userId = user.getId(); + + sync(user); + + Authorizable authorizable = userManager.getAuthorizable(userId); + assertNotNull(authorizable); + assertFalse(((User)authorizable).isDisabled()); + assertTrue(authorizable.declaredMemberOf().hasNext()); + + // remove user externally + removeIDPUser(userId); + + // test sync with 'keepmissing' = true + syncCtx.setKeepMissing(true); + SyncResult result = syncCtx.sync(userId); + assertEquals(SyncResult.Status.MISSING, result.getStatus()); + assertNotNull(userManager.getAuthorizable(userId)); + + // test sync with 'keepmissing' = false + syncCtx.setKeepMissing(false); + result = syncCtx.sync(userId); + assertEquals(SyncResult.Status.DISABLE, result.getStatus()); + + authorizable = userManager.getAuthorizable(userId); + assertNotNull(authorizable); + assertTrue(((User)authorizable).isDisabled()); + // verify external group membership is removed + assertFalse(authorizable.declaredMemberOf().hasNext()); + + // add external user back + addIDPUser(user); + + result = syncCtx.sync(userId); + assertEquals(SyncResult.Status.ENABLE, result.getStatus()); + assertNotNull(userManager.getAuthorizable(userId)); + assertFalse(((User)authorizable).isDisabled()); + // verify external group membership is added back + assertTrue(authorizable.declaredMemberOf().hasNext()); + } + + @Test + public void testSyncDoesNotEnableUsers() throws Exception { + // configure to remove missing users, check that sync does not mess with disabled status + syncConfig.user().setDisableMissing(false); + // test sync with 'keepmissing' = false + syncCtx.setKeepMissing(false); + syncCtx.setForceUserSync(true); + + ExternalUser user = idp.listUsers().next(); + assertNotNull(user); + + SyncResult result = syncCtx.sync(user); + assertEquals(SyncResult.Status.ADD, result.getStatus()); + + Authorizable authorizable = userManager.getAuthorizable(user.getId()); + assertTrue(authorizable instanceof User); + User u = (User) authorizable; + + // disable user + u.disable("disabled locally"); + root.commit(); + + // sync + result = syncCtx.sync(user.getId()); + assertEquals(SyncResult.Status.UPDATE, result.getStatus()); + authorizable = userManager.getAuthorizable(user.getId()); + assertNotNull(authorizable); + assertTrue(authorizable instanceof User); + assertTrue(((User)authorizable).isDisabled()); + } + + @Test public void testSyncGroupById() throws Exception { ExternalIdentity externalId = idp.listGroups().next(); \ No newline at end of file Index: src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/AbstractExternalAuthTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/AbstractExternalAuthTest.java (revision 763a737254cde4c1a8ff9d59034089758c5e7fdc) +++ src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/AbstractExternalAuthTest.java (revision ) @@ -144,6 +144,18 @@ // nothing to do } + protected void addIDPUser(String id) { + ((TestIdentityProvider) idp).addUser(new TestIdentityProvider.TestUser(id, idp.getName())); + } + + protected void addIDPUser(TestIdentityProvider.TestUser user) { + ((TestIdentityProvider) idp).addUser(user); + } + + protected void removeIDPUser(String id) { + ((TestIdentityProvider) idp).removeUser(id); + } + protected DefaultSyncConfig createSyncConfig() { DefaultSyncConfig syncConfig = new DefaultSyncConfig(); Map mapping = new HashMap(); \ No newline at end of file Index: src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfigImpl.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfigImpl.java (revision 763a737254cde4c1a8ff9d59034089758c5e7fdc) +++ src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfigImpl.java (revision ) @@ -178,6 +178,22 @@ public static final String PARAM_USER_DYNAMIC_MEMBERSHIP = "user.dynamicMembership"; /** + * @see User#getDisableMissing() + */ + public static final boolean PARAM_DISABLE_MISSING_USERS_DEFAULT = false; + + /** + * @see User#getDisableMissing() + */ + @Property( + label = "Disable missing users", + description = "If true, users that no longer exist on the external provider will be locally disabled, " + + "and re-enabled if they become valid again. If false (default) they will be removed.", + boolValue = false + ) + public static final String PARAM_DISABLE_MISSING_USERS = "user.disableMissing"; + + /** * @see DefaultSyncConfig.Group#getExpirationTime() */ public static final String PARAM_GROUP_EXPIRATION_TIME_DEFAULT = "1d"; @@ -268,6 +284,7 @@ .setName(params.getConfigValue(PARAM_NAME, PARAM_NAME_DEFAULT)); cfg.user() + .setDisableMissing(params.getConfigValue(PARAM_DISABLE_MISSING_USERS, PARAM_DISABLE_MISSING_USERS_DEFAULT)) .setMembershipExpirationTime(getMilliSeconds(params, PARAM_USER_MEMBERSHIP_EXPIRATION_TIME, PARAM_USER_MEMBERSHIP_EXPIRATION_TIME_DEFAULT, ONE_HOUR)) .setMembershipNestingDepth(params.getConfigValue(PARAM_USER_MEMBERSHIP_NESTING_DEPTH, PARAM_USER_MEMBERSHIP_NESTING_DEPTH_DEFAULT)) .setDynamicMembership(params.getConfigValue(PARAM_USER_DYNAMIC_MEMBERSHIP, PARAM_USER_DYNAMIC_MEMBERSHIP_DEFAULT)) \ No newline at end of file Index: src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncResult.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncResult.java (revision 763a737254cde4c1a8ff9d59034089758c5e7fdc) +++ src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncResult.java (revision ) @@ -64,6 +64,16 @@ DELETE, /** + * authorizable enabled + */ + ENABLE, + + /** + * authorizable disabled + */ + DISABLE, + + /** * nothing changed. no such authorizable found. */ NO_SUCH_AUTHORIZABLE, \ No newline at end of file Index: src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/TestIdentityProvider.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/TestIdentityProvider.java (revision 763a737254cde4c1a8ff9d59034089758c5e7fdc) +++ src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/TestIdentityProvider.java (revision ) @@ -81,12 +81,16 @@ .withGroups("_gr_u_", "g%r%")); } - private void addUser(TestIdentity user) { - externalUsers.put(user.getId().toLowerCase(), (TestUser) user); + public void addUser(TestIdentity user) { + externalUsers.put(user.getId().toLowerCase(), (ExternalUser) user); } + public void removeUser(String id) { + externalUsers.remove(id.toLowerCase()); + } + private void addGroup(TestIdentity group) { - externalGroups.put(group.getId().toLowerCase(), (TestGroup) group); + externalGroups.put(group.getId().toLowerCase(), (ExternalGroup) group); } @Nonnull \ No newline at end of file Index: src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncContext.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncContext.java (revision 763a737254cde4c1a8ff9d59034089758c5e7fdc) +++ src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncContext.java (revision ) @@ -325,11 +325,30 @@ if (authorizable.isGroup() && ((Group) authorizable).getDeclaredMembers().hasNext()) { log.info("won't remove local group with members: {}", id); status = SyncResult.Status.NOP; + } else if (!keepMissing) { + status = SyncResult.Status.DELETE; + + if (authorizable instanceof User) { + User user = (User) authorizable; + if (config.user().getDisableMissing()) { + user.disable("No longer exists on external identity provider '" + idp.getName() + "'"); + log.debug("disabling user '{}' that no longer exists on IDP {}", id, idp.getName()); + + // remove user from groups + syncMembership(null, user, config.user().getMembershipNestingDepth()); + status = SyncResult.Status.DISABLE; + + } else { + user.remove(); + log.debug("removing user '{}' that no longer exists on IDP {}", id, idp.getName()); + } + } else { - authorizable.remove(); - log.debug("removing authorizable '{}' that no longer exists on IDP {}", id, idp.getName()); + authorizable.remove(); + log.debug("removing authorizable '{}' that no longer exists on IDP {}", id, idp.getName()); + } timer.mark("remove"); - status = SyncResult.Status.DELETE; + } else { status = SyncResult.Status.MISSING; log.info("external identity missing for {}, but purge == false.", id); @@ -432,9 +451,21 @@ // synchronize external memberships syncMembership(external, user, config.user().getMembershipNestingDepth()); } + + status = SyncResult.Status.UPDATE; + + if (this.config.user().getDisableMissing()) { + // ensure users get re-enabled + if (user.isDisabled()) { + status = SyncResult.Status.ENABLE; + user.disable(null); + // reestablish memberships + syncMembership(external, user, config.user().getMembershipNestingDepth()); + } + } + // finally "touch" the sync property user.setProperty(REP_LAST_SYNCED, nowValue); - status = SyncResult.Status.UPDATE; } return new DefaultSyncResultImpl(createSyncedIdentity(user), status); } @@ -480,19 +511,23 @@ * @param depth recursion depth. * @throws RepositoryException */ - protected void syncMembership(@Nonnull ExternalIdentity external, @Nonnull Authorizable auth, long depth) + protected void syncMembership(@Nullable ExternalIdentity external, @Nonnull Authorizable auth, long depth) throws RepositoryException { if (depth <= 0) { return; } if (log.isDebugEnabled()) { - log.debug("Syncing membership '{}' -> '{}'", external.getExternalId().getString(), auth.getID()); + log.debug("Syncing membership '{}' -> '{}'", external != null ? external.getExternalId().getString() : "", auth.getID()); } final DebugTimer timer = new DebugTimer(); Iterable externalGroups; try { + if (external != null) { - externalGroups = external.getDeclaredGroups(); + externalGroups = external.getDeclaredGroups(); + } else { + externalGroups = new ArrayList<>(); + } } catch (ExternalIdentityException e) { log.error("Error while retrieving external declared groups for '{}'", external.getId(), e); return; \ No newline at end of file