You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by mc...@apache.org on 2019/09/18 13:09:15 UTC

[nifi] branch master updated: NIFI-6400 Better options, consistent ids for ShellUserGroupProvider. NIFI-6400 Fixes to address PR feedback. NIFI-6400 Accepts proposed changes.

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

mcgilman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/master by this push:
     new 4596ef7  NIFI-6400 Better options, consistent ids for ShellUserGroupProvider. NIFI-6400 Fixes to address PR feedback. NIFI-6400 Accepts proposed changes.
4596ef7 is described below

commit 4596ef7c8a1b4a002dd81d210ceab4f99bc32fd6
Author: Troy Melhase <tr...@troy.io>
AuthorDate: Tue Aug 6 13:33:05 2019 -0800

    NIFI-6400 Better options, consistent ids for ShellUserGroupProvider.
    NIFI-6400 Fixes to address PR feedback.
    NIFI-6400 Accepts proposed changes.
    
    This closes #3637
---
 .../src/main/asciidoc/administration-guide.adoc    |   2 +
 .../src/main/resources/conf/authorizers.xml        |   6 +-
 .../nifi/authorization/ShellUserGroupProvider.java | 100 +++++----
 .../authorization/ShellUserGroupProviderBase.java  | 176 ----------------
 .../authorization/ShellUserGroupProviderIT.java    | 234 +++++++++++++++------
 5 files changed, 234 insertions(+), 284 deletions(-)

diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index 9715ab0..24d9720 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -469,6 +469,8 @@ The ShellUserGroupProvider has the following properties:
 | Property Name | Description
 |`Initial Refresh Delay` | Duration of initial delay before first user and group refresh. (i.e. `10 secs`).  Default is `5 mins`.
 |`Refresh Delay` | Duration of delay between each user and group refresh. (i.e. `10 secs`).  Default is `5 mins`.
+|`Exclude Groups` | Regular expression used to exclude groups.  Default is '', which means no groups are excluded.
+|`Exclude Users` | Regular expression used to exclude users.  Default is '', which means no users are excluded.
 |==================================================================================================================================================
 
 Like LdapUserGroupProvider, the ShellUserGroupProvider is commented out in the _authorizers.xml_ file.  Refer to that comment for usage examples.
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/authorizers.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/authorizers.xml
index 3ee04b9..52f9bb6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/authorizers.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/conf/authorizers.xml
@@ -175,15 +175,17 @@
         on systems that support `sh`.  Implementations available for Linux and Mac OS, and are selected by the
         provider based on the system property `os.name`.
 
-        'Initial Refresh Delay' - duration to wait before first refresh.  Default is '5 mins'.
         'Refresh Delay' - duration to wait between subsequent refreshes.  Default is '5 mins'.
+        'Exclude Groups' - regular expression used to exclude groups.  Default is '', which means no groups are excluded.
+        'Exclude Users' - regular expression used to exclude users.  Default is '', which means no users are excluded.
     -->
     <!-- To enable the shell-user-group-provider remove 2 lines. This is 1 of 2.
     <userGroupProvider>
         <identifier>shell-user-group-provider</identifier>
         <class>org.apache.nifi.authorization.ShellUserGroupProvider</class>
-        <property name="Initial Refresh Delay">5 mins</property>
         <property name="Refresh Delay">5 mins</property>
+        <property name="Exclude Groups"></property>
+        <property name="Exclude Users"></property>
     </userGroupProvider>
     To enable the shell-user-group-provider remove 2 lines. This is 2 of 2. -->
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-shell-authorizer/src/main/java/org/apache/nifi/authorization/ShellUserGroupProvider.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-shell-authorizer/src/main/java/org/apache/nifi/authorization/ShellUserGroupProvider.java
index e499b6e..d48255e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-shell-authorizer/src/main/java/org/apache/nifi/authorization/ShellUserGroupProvider.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-shell-authorizer/src/main/java/org/apache/nifi/authorization/ShellUserGroupProvider.java
@@ -16,6 +16,15 @@
  */
 package org.apache.nifi.authorization;
 
+import org.apache.nifi.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.authorization.exception.AuthorizerCreationException;
+import org.apache.nifi.authorization.exception.AuthorizerDestructionException;
+import org.apache.nifi.authorization.util.ShellRunner;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.util.FormatUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -26,21 +35,14 @@ import java.util.Set;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
-import org.apache.nifi.authorization.exception.AuthorizationAccessException;
-import org.apache.nifi.authorization.exception.AuthorizerCreationException;
-import org.apache.nifi.authorization.exception.AuthorizerDestructionException;
-import org.apache.nifi.authorization.util.ShellRunner;
-import org.apache.nifi.components.PropertyValue;
-import org.apache.nifi.util.FormatUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 
 
 /*
  * ShellUserGroupProvider implements UserGroupProvider by way of shell commands.
  */
 public class ShellUserGroupProvider implements UserGroupProvider {
-
     private final static Logger logger = LoggerFactory.getLogger(ShellUserGroupProvider.class);
 
     private final static String OS_TYPE_ERROR = "Unsupported operating system.";
@@ -49,12 +51,15 @@ public class ShellUserGroupProvider implements UserGroupProvider {
     private final static Map<String, User> usersByName = new HashMap<>(); // name == identity
     private final static Map<String, Group> groupsById = new HashMap<>();
 
-    public static final String INITIAL_REFRESH_DELAY_PROPERTY = "Initial Refresh Delay";
     public static final String REFRESH_DELAY_PROPERTY = "Refresh Delay";
-
     private static final long MINIMUM_SYNC_INTERVAL_MILLISECONDS = 10_000;
-    private long initialDelay;
+
+    public static final String EXCLUDE_USER_PROPERTY = "Exclude Users";
+    public static final String EXCLUDE_GROUP_PROPERTY = "Exclude Groups";
+
     private long fixedDelay;
+    private Pattern excludeUsers;
+    private Pattern excludeGroups;
 
     // Our scheduler has one thread for users, one for groups:
     private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
@@ -99,11 +104,6 @@ public class ShellUserGroupProvider implements UserGroupProvider {
         }
 
         if (user == null) {
-            refreshOneUser(selectedShellCommands.getUserById(identifier), "Get Single User by Id");
-            user = usersById.get(identifier);
-        }
-
-        if (user == null) {
             logger.debug("getUser (by id) user not found: " + identifier);
         } else {
             logger.debug("getUser (by id) found user: " + user + " for id: " + identifier);
@@ -235,7 +235,6 @@ public class ShellUserGroupProvider implements UserGroupProvider {
      */
     @Override
     public void onConfigured(AuthorizerConfigurationContext configurationContext) throws AuthorizerCreationException {
-        initialDelay = getDelayProperty(configurationContext, INITIAL_REFRESH_DELAY_PROPERTY, "5 mins");
         fixedDelay = getDelayProperty(configurationContext, REFRESH_DELAY_PROPERTY, "5 mins");
 
         // Our next init step is to select the command set based on the operating system name:
@@ -255,11 +254,26 @@ public class ShellUserGroupProvider implements UserGroupProvider {
             throw new AuthorizerCreationException(SYS_CHECK_ERROR, ioexc.getCause());
         }
 
+        // The next step is to add the user and group exclude regexes:
+        try {
+            excludeGroups = Pattern.compile(getProperty(configurationContext, EXCLUDE_GROUP_PROPERTY, ""));
+            excludeUsers = Pattern.compile(getProperty(configurationContext, EXCLUDE_USER_PROPERTY, ""));
+        } catch (final PatternSyntaxException e) {
+            throw new AuthorizerCreationException(e);
+        }
+
         // With our command set selected, and our system check passed, we can pull in the users and groups:
         refreshUsersAndGroups();
 
         // And finally, our last init step is to fire off the refresh thread:
-        scheduler.scheduleWithFixedDelay(this::refreshUsersAndGroups, initialDelay, fixedDelay, TimeUnit.SECONDS);
+        scheduler.scheduleWithFixedDelay(() -> {
+            try {
+                refreshUsersAndGroups();
+            }catch (final Throwable t) {
+                logger.error("", t);
+            }
+        }, fixedDelay, fixedDelay, TimeUnit.SECONDS);
+
     }
 
     private static ShellCommandsProvider getCommandsProviderFromName(String osName) {
@@ -280,6 +294,19 @@ public class ShellUserGroupProvider implements UserGroupProvider {
         return commands;
     }
 
+    private String getProperty(AuthorizerConfigurationContext authContext, String propertyName, String defaultValue) {
+        final PropertyValue property = authContext.getProperty(propertyName);
+        final String value;
+
+        if (property != null && property.isSet()) {
+            value = property.getValue();
+        } else {
+            value = defaultValue;
+        }
+        return value;
+
+    }
+
     private long getDelayProperty(AuthorizerConfigurationContext authContext, String propertyName, String defaultValue) {
         final PropertyValue intervalProperty = authContext.getProperty(propertyName);
         final String propertyValue;
@@ -445,20 +472,21 @@ public class ShellUserGroupProvider implements UserGroupProvider {
             if (record.length > 2) {
                 String name = record[0], id = record[1], gid = record[2];
 
-                if (name != null && id != null && !name.equals("") && !id.equals("")) {
-
-                    User user = new User.Builder().identity(name).identifier(id).build();
-                    idToUser.put(id, user);
+                if (name != null && id != null && !name.equals("") && !id.equals("") && !excludeUsers.matcher(name).matches()) {
+                    User user = new User.Builder().identity(name).identifierGenerateFromSeed(id).build();
+                    idToUser.put(user.getIdentifier(), user);
                     usernameToUser.put(name, user);
 
                     if (gid != null && !gid.equals("")) {
-                        gidToUser.put(gid, user);
+                        // create a temporary group to deterministically generate the group id and associate this user
+                        Group group = new Group.Builder().name(gid).identifierGenerateFromSeed(gid).build();
+                        gidToUser.put(group.getIdentifier(), user);
                     } else {
                         logger.warn("Null or empty primary group id for: " + name);
                     }
 
                 } else {
-                    logger.warn("Null or empty user name: " + name + " or id: " + id);
+                    logger.warn("Null, empty, or skipped user name: " + name + " or id: " + id);
                 }
             } else {
                 logger.warn("Unexpected record format.  Expected 3 or more colon separated values per line.");
@@ -499,12 +527,12 @@ public class ShellUserGroupProvider implements UserGroupProvider {
                     logger.error("list membership shell exception: " + ioexc);
                 }
 
-                if (name != null && id != null && !name.equals("") && !id.equals("")) {
-                    Group group = new Group.Builder().name(name).identifier(id).addUsers(users).build();
-                    groupsById.put(id, group);
+                if (name != null && id != null && !name.equals("") && !id.equals("") && !excludeGroups.matcher(name).matches()) {
+                    Group group = new Group.Builder().name(name).identifierGenerateFromSeed(id).addUsers(users).build();
+                    groupsById.put(group.getIdentifier(), group);
                     logger.debug("Refreshed group: " + group);
                 } else {
-                    logger.warn("Null or empty group name: " + name + " or id: " + id);
+                    logger.warn("Null, empty, or skipped group name: " + name + " or id: " + id);
                 }
             } else {
                 logger.warn("Unexpected record format.  Expected 1 or more comma separated values.");
@@ -525,27 +553,19 @@ public class ShellUserGroupProvider implements UserGroupProvider {
 
             if (primaryGroup == null) {
                 logger.warn("user: " + primaryUser + " primary group not found");
-            } else {
+            } else if (!excludeGroups.matcher(primaryGroup.getName()).matches()) {
                 Set<String> groupUsers = primaryGroup.getUsers();
                 if (!groupUsers.contains(primaryUser.getIdentity())) {
                     Set<String> secondSet = new HashSet<>(groupUsers);
                     secondSet.add(primaryUser.getIdentity());
-                    Group group = new Group.Builder().name(primaryGroup.getName()).identifier(primaryGid).addUsers(secondSet).build();
-                    gidToGroup.put(primaryGid, group);
+                    Group group = new Group.Builder().name(primaryGroup.getName()).identifierGenerateFromSeed(primaryGid).addUsers(secondSet).build();
+                    gidToGroup.put(group.getIdentifier(), group);
                 }
             }
         });
     }
 
     /**
-     * @return The initial refresh delay.
-     */
-    public long getInitialRefreshDelay() {
-        return initialDelay;
-    }
-
-
-    /**
      * @return The fixed refresh delay.
      */
     public long getRefreshDelay() {
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-shell-authorizer/src/test/java/org/apache/nifi/authorization/ShellUserGroupProviderBase.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-shell-authorizer/src/test/java/org/apache/nifi/authorization/ShellUserGroupProviderBase.java
deleted file mode 100644
index fa760b2..0000000
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-shell-authorizer/src/test/java/org/apache/nifi/authorization/ShellUserGroupProviderBase.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * 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.nifi.authorization;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
-
-import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-
-abstract class ShellUserGroupProviderBase {
-    private static final Logger logger = LoggerFactory.getLogger(ShellUserGroupProviderBase.class);
-
-    private final String KNOWN_USER  = "root";
-    private final String KNOWN_UID   = "0";
-
-    @SuppressWarnings("FieldCanBeLocal")
-    private final String KNOWN_GROUP = "root";
-
-    @SuppressWarnings("FieldCanBeLocal")
-    private final String OTHER_GROUP = "wheel"; // e.g., macos
-    private final String KNOWN_GID   = "0";
-
-    // We're using this knob to control the test runs on Travis.  The issue there is that tests
-    // running on Travis do not have `getent`, thus not behaving like a typical Linux installation.
-    protected static boolean systemCheckFailed = false;
-
-    /**
-     * Ensures that the test can run because Docker is available and the remote instance can be reached via ssh.
-     *
-     * @return true if Docker is available on this OS
-     */
-    protected boolean isSSHAvailable() {
-        return !systemCheckFailed;
-    }
-
-
-    /**
-     * Tests the provider behavior by getting its users and checking minimum size.
-     *
-     * @param provider {@link UserGroupProvider}
-     */
-    void testGetUsersAndUsersMinimumCount(UserGroupProvider provider) {
-        assumeTrue(isSSHAvailable());
-
-        Set<User> users = provider.getUsers();
-        assertNotNull(users);
-        assertTrue(users.size() > 0);
-    }
-
-
-    /**
-     * Tests the provider behavior by getting a known user by uid.
-     *
-     * @param provider {@link UserGroupProvider}
-     */
-    void testGetKnownUserByUsername(UserGroupProvider provider) {
-        // assumeTrue(isSSHAvailable());
-
-        User root = provider.getUser(KNOWN_UID);
-        assertNotNull(root);
-        assertEquals(KNOWN_USER, root.getIdentity());
-        assertEquals(KNOWN_UID, root.getIdentifier());
-    }
-
-    /**
-     * Tests the provider behavior by getting a known user by id.
-     *
-     * @param provider {@link UserGroupProvider}
-     */
-    void testGetKnownUserByUid(UserGroupProvider provider) {
-        assumeTrue(isSSHAvailable());
-
-        User root = provider.getUserByIdentity(KNOWN_USER);
-        assertNotNull(root);
-        assertEquals(KNOWN_USER, root.getIdentity());
-        assertEquals(KNOWN_UID, root.getIdentifier());
-    }
-
-    /**
-     * Tests the provider behavior by getting its groups and checking minimum size.
-     *
-     * @param provider {@link UserGroupProvider}
-     */
-    void testGetGroupsAndMinimumGroupCount(UserGroupProvider provider) {
-        assumeTrue(isSSHAvailable());
-
-        Set<Group> groups = provider.getGroups();
-        assertNotNull(groups);
-        assertTrue(groups.size() > 0);
-    }
-
-    /**
-     * Tests the provider behavior by getting a known group by GID.
-     *
-     * @param provider {@link UserGroupProvider}
-     */
-    void testGetKnownGroupByGid(UserGroupProvider provider) {
-        assumeTrue(isSSHAvailable());
-
-        Group group = provider.getGroup(KNOWN_GID);
-        assertNotNull(group);
-        assertTrue(group.getName().equals(KNOWN_GROUP) || group.getName().equals(OTHER_GROUP));
-        assertEquals(KNOWN_GID, group.getIdentifier());
-    }
-
-    /**
-     * Tests the provider behavior by getting a known group and checking for a known member of it.
-     *
-     * @param provider {@link UserGroupProvider}
-     */
-    void testGetGroupByGidAndGetGroupMembership(UserGroupProvider provider) {
-        assumeTrue(isSSHAvailable());
-
-        Group group = provider.getGroup(KNOWN_GID);
-        assertNotNull(group);
-
-        // These next few try/catch blocks are here for debugging.  The user-to-group relationship
-        // is delicate with this implementation, and this approach allows us a measure of control.
-        // Check your logs if you're having problems!
-
-        try {
-            assertTrue(group.getUsers().size() > 0);
-            logger.info("root group count: " + group.getUsers().size());
-        } catch (final AssertionError ignored) {
-            logger.info("root group count zero on this system");
-        }
-
-        try {
-            assertTrue(group.getUsers().contains(KNOWN_USER));
-            logger.info("root group membership: " + group.getUsers());
-        } catch (final AssertionError ignored) {
-            logger.info("root group membership unexpected on this system");
-        }
-    }
-
-    /**
-     * Tests the provider behavior by getting a known user and checking its group membership.
-     *
-     * @param provider {@link UserGroupProvider}
-     */
-    void testGetUserByIdentityAndGetGroupMembership(UserGroupProvider provider) {
-        assumeTrue(isSSHAvailable());
-
-        UserAndGroups user = provider.getUserAndGroups(KNOWN_USER);
-        assertNotNull(user);
-
-        try {
-            assertTrue(user.getGroups().size() > 0);
-            logger.info("root user group count: " + user.getGroups().size());
-        } catch (final AssertionError ignored) {
-            logger.info("root user and groups group count zero on this system");
-        }
-
-        Set<Group> groups = provider.getGroups();
-        assertTrue(groups.size() > user.getGroups().size());
-    }
-}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-shell-authorizer/src/test/java/org/apache/nifi/authorization/ShellUserGroupProviderIT.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-shell-authorizer/src/test/java/org/apache/nifi/authorization/ShellUserGroupProviderIT.java
index 1525ae5..65bb884 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-shell-authorizer/src/test/java/org/apache/nifi/authorization/ShellUserGroupProviderIT.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-shell-authorizer/src/test/java/org/apache/nifi/authorization/ShellUserGroupProviderIT.java
@@ -20,6 +20,8 @@ import java.io.File;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
+
 import org.apache.nifi.authorization.exception.AuthorizerCreationException;
 import org.apache.nifi.authorization.util.ShellRunner;
 import org.apache.nifi.util.MockPropertyValue;
@@ -29,6 +31,7 @@ import org.junit.Assume;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.ClassRule;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
@@ -40,7 +43,7 @@ import org.testcontainers.containers.GenericContainer;
 import org.testcontainers.utility.MountableFile;
 
 
-public class ShellUserGroupProviderIT extends ShellUserGroupProviderBase {
+public class ShellUserGroupProviderIT {
     private static final Logger logger = LoggerFactory.getLogger(ShellUserGroupProviderIT.class);
 
     // These images are publicly available on the hub.docker.com, and the source to each
@@ -61,10 +64,24 @@ public class ShellUserGroupProviderIT extends ShellUserGroupProviderBase {
     private final static String CONTAINER_SSH_AUTH_KEYS = "/root/.ssh/authorized_keys";
     private final static Integer CONTAINER_SSH_PORT = 22;
 
+    private final String KNOWN_USER  = "root";
+    private final String KNOWN_UID   = "0";
+
+    @SuppressWarnings("FieldCanBeLocal")
+    private final String KNOWN_GROUP = "root";
+
+    @SuppressWarnings("FieldCanBeLocal")
+    private final String OTHER_GROUP = "wheel"; // e.g., macos
+    private final String KNOWN_GID   = "0";
+
+    // We're using this knob to control the test runs on Travis.  The issue there is that tests
+    // running on Travis do not have `getent`, thus not behaving like a typical Linux installation.
+    protected static boolean systemCheckFailed = false;
+
     private static String sshPrivKeyFile;
     private static String sshPubKeyFile;
 
-    private AuthorizerConfigurationContext authContext;
+    private AuthorizerConfigurationContext authContext = Mockito.mock(AuthorizerConfigurationContext.class);
     private ShellUserGroupProvider localProvider;
     private UserGroupProviderInitializationContext initContext;
 
@@ -103,8 +120,9 @@ public class ShellUserGroupProviderIT extends ShellUserGroupProviderBase {
         authContext = Mockito.mock(AuthorizerConfigurationContext.class);
         initContext = Mockito.mock(UserGroupProviderInitializationContext.class);
 
-        Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.INITIAL_REFRESH_DELAY_PROPERTY))).thenReturn(new MockPropertyValue("10 sec"));
-        Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.REFRESH_DELAY_PROPERTY))).thenReturn(new MockPropertyValue("15 sec"));
+        Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.REFRESH_DELAY_PROPERTY))).thenReturn(new MockPropertyValue("10 sec"));
+        Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.EXCLUDE_GROUP_PROPERTY))).thenReturn(new MockPropertyValue(".*d$"));
+        Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.EXCLUDE_USER_PROPERTY))).thenReturn(new MockPropertyValue("^s.*"));
 
         localProvider = new ShellUserGroupProvider();
         try {
@@ -115,8 +133,7 @@ public class ShellUserGroupProviderIT extends ShellUserGroupProviderBase {
             logger.error("setup() exception: " + exc + "; tests cannot run on this system.");
             return;
         }
-        Assert.assertEquals(10000, localProvider.getInitialRefreshDelay());
-        Assert.assertEquals(15000, localProvider.getRefreshDelay());
+        Assert.assertEquals(10000, localProvider.getRefreshDelay());
     }
 
     @After
@@ -124,45 +141,6 @@ public class ShellUserGroupProviderIT extends ShellUserGroupProviderBase {
         localProvider.preDestruction();
     }
 
-    // Our primary test methods all accept a provider; here we define overloads to those methods to
-    // use the local provider.  This allows the reuse of those test methods with the remote provider.
-
-    @Test
-    public void testGetUsersAndUsersMinimumCount() {
-        testGetUsersAndUsersMinimumCount(localProvider);
-    }
-
-    @Test
-    public void testGetKnownUserByUsername() {
-        testGetKnownUserByUsername(localProvider);
-    }
-
-    @Test
-    public void testGetKnownUserByUid() {
-        testGetKnownUserByUid(localProvider);
-    }
-
-    @Test
-    public void testGetGroupsAndMinimumGroupCount() {
-        testGetGroupsAndMinimumGroupCount(localProvider);
-    }
-
-    @Test
-    public void testGetKnownGroupByGid() {
-        testGetKnownGroupByGid(localProvider);
-    }
-
-    @Test
-    public void testGetGroupByGidAndGetGroupMembership() {
-        testGetGroupByGidAndGetGroupMembership(localProvider);
-    }
-
-    @Test
-    public void testGetUserByIdentityAndGetGroupMembership() {
-        testGetUserByIdentityAndGetGroupMembership(localProvider);
-    }
-
-    @SuppressWarnings("RedundantThrows")
     private GenericContainer createContainer(String image) throws IOException, InterruptedException {
         GenericContainer container = new GenericContainer(image)
             .withEnv("SSH_ENABLE_ROOT", "true").withExposedPorts(CONTAINER_SSH_PORT);
@@ -192,22 +170,59 @@ public class ShellUserGroupProviderIT extends ShellUserGroupProviderBase {
     public void testTooShortDelayIntervalThrowsException() throws AuthorizerCreationException {
         final AuthorizerConfigurationContext authContext = Mockito.mock(AuthorizerConfigurationContext.class);
         final ShellUserGroupProvider localProvider = new ShellUserGroupProvider();
-        Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.INITIAL_REFRESH_DELAY_PROPERTY))).thenReturn(new MockPropertyValue("1 milliseconds"));
+        Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.REFRESH_DELAY_PROPERTY))).thenReturn(new MockPropertyValue("1 milliseconds"));
 
         expectedException.expect(AuthorizerCreationException.class);
-        expectedException.expectMessage("The Initial Refresh Delay '1 milliseconds' is below the minimum value of '10000 ms'");
+        expectedException.expectMessage("The Refresh Delay '1 milliseconds' is below the minimum value of '10000 ms'");
 
         localProvider.onConfigured(authContext);
     }
 
+
+    @Test
+    public void testInvalidGroupExcludeExpressionsThrowsException() throws AuthorizerCreationException {
+        AuthorizerConfigurationContext authContext = Mockito.mock(AuthorizerConfigurationContext.class);
+        ShellUserGroupProvider localProvider = new ShellUserGroupProvider();
+        Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.REFRESH_DELAY_PROPERTY))).thenReturn(new MockPropertyValue("3 minutes"));
+        Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.EXCLUDE_GROUP_PROPERTY))).thenReturn(new MockPropertyValue("(3"));
+
+        expectedException.expect(AuthorizerCreationException.class);
+        expectedException.expectMessage("Unclosed group near index");
+        localProvider.onConfigured(authContext);
+
+
+    }
+
+    @Test
+    public void testInvalidUserExcludeExpressionsThrowsException() throws AuthorizerCreationException {
+        AuthorizerConfigurationContext authContext = Mockito.mock(AuthorizerConfigurationContext.class);
+        ShellUserGroupProvider localProvider = new ShellUserGroupProvider();
+        Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.REFRESH_DELAY_PROPERTY))).thenReturn(new MockPropertyValue("3 minutes"));
+        Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.EXCLUDE_USER_PROPERTY))).thenReturn(new MockPropertyValue("*"));
+
+        expectedException.expect(AuthorizerCreationException.class);
+        expectedException.expectMessage("Dangling meta character");
+        localProvider.onConfigured(authContext);
+    }
+
+    @Test
+    public void testMissingExcludeExpressionsAllowed() throws AuthorizerCreationException {
+        AuthorizerConfigurationContext authContext = Mockito.mock(AuthorizerConfigurationContext.class);
+        ShellUserGroupProvider localProvider = new ShellUserGroupProvider();
+        Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.REFRESH_DELAY_PROPERTY))).thenReturn(new MockPropertyValue("3 minutes"));
+
+        localProvider.onConfigured(authContext);
+        verifyUsersAndUsersMinimumCount(localProvider);
+    }
+
     @Test
     public void testInvalidDelayIntervalThrowsException() throws AuthorizerCreationException {
         final AuthorizerConfigurationContext authContext = Mockito.mock(AuthorizerConfigurationContext.class);
         final ShellUserGroupProvider localProvider = new ShellUserGroupProvider();
-        Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.INITIAL_REFRESH_DELAY_PROPERTY))).thenReturn(new MockPropertyValue("Not an interval"));
+        Mockito.when(authContext.getProperty(Mockito.eq(ShellUserGroupProvider.REFRESH_DELAY_PROPERTY))).thenReturn(new MockPropertyValue("Not an interval"));
 
         expectedException.expect(AuthorizerCreationException.class);
-        expectedException.expectMessage("The Initial Refresh Delay 'Not an interval' is not a valid time interval.");
+        expectedException.expectMessage("The Refresh Delay 'Not an interval' is not a valid time interval.");
 
         localProvider.onConfigured(authContext);
     }
@@ -220,24 +235,27 @@ public class ShellUserGroupProviderIT extends ShellUserGroupProviderBase {
     }
 
     @Test
-    public void testGetOneUserAfterClearingCaches() {
-        // assert known state:  empty, testable, not empty
-        localProvider.clearCaches();
-        testGetKnownUserByUid(localProvider);
-        assert localProvider.userCacheSize() > 0;
+    public void testLocalGetUsersAndUsersMinimumCount() {
+        verifyUsersAndUsersMinimumCount(localProvider);
     }
 
     @Test
-    public void testGetOneGroupAfterClearingCaches() {
-        Assume.assumeTrue(isSSHAvailable());
+    public void testLocalGetKnownUserByUsername() {
+        verifyKnownUserByUsername(localProvider);
+    }
 
-        // assert known state:  empty, testable, not empty
-        localProvider.clearCaches();
-        testGetKnownGroupByGid(localProvider);
-        assert localProvider.groupCacheSize() > 0;
+    @Test
+    public void testLocalGetGroupsAndMinimumGroupCount() {
+        verifyGroupsAndMinimumGroupCount(localProvider);
     }
 
     @Test
+    public void testLocalGetUserByIdentityAndGetGroupMembership() {
+        verifyGetUserByIdentityAndGetGroupMembership(localProvider);
+    }
+
+    // @Ignore // for now
+    @Test
     public void testVariousSystemImages() {
         // Here we explicitly clear the system check flag to allow the remote checks that follow:
         systemCheckFailed = false;
@@ -262,12 +280,10 @@ public class ShellUserGroupProviderIT extends ShellUserGroupProviderBase {
                 }
 
                 try {
-                    testGetUsersAndUsersMinimumCount(remoteProvider);
-                    testGetKnownUserByUsername(remoteProvider);
-                    testGetGroupsAndMinimumGroupCount(remoteProvider);
-                    testGetKnownGroupByGid(remoteProvider);
-                    testGetGroupByGidAndGetGroupMembership(remoteProvider);
-                    testGetUserByIdentityAndGetGroupMembership(remoteProvider);
+                    verifyUsersAndUsersMinimumCount(remoteProvider);
+                    verifyKnownUserByUsername(remoteProvider);
+                    verifyGroupsAndMinimumGroupCount(remoteProvider);
+                    verifyGetUserByIdentityAndGetGroupMembership(remoteProvider);
                 } catch (final Exception e) {
                     // Some environments don't allow our tests to work.
                     logger.error("Exception running remote provider on image: " + image +  ", exception: " + e);
@@ -280,4 +296,90 @@ public class ShellUserGroupProviderIT extends ShellUserGroupProviderBase {
     }
 
     // TODO: Make test which retrieves list of users and then getUserByIdentity to ensure the user is populated in the response
+
+
+
+    /**
+     * Ensures that the test can run because Docker is available and the remote instance can be reached via ssh.
+     *
+     * @return true if Docker is available on this OS
+     */
+    private boolean isSSHAvailable() {
+        return !systemCheckFailed;
+    }
+
+    /**
+     * Tests the provider behavior by getting its users and checking minimum size.
+     *
+     * @param provider {@link UserGroupProvider}
+     */
+    private void verifyUsersAndUsersMinimumCount(UserGroupProvider provider) {
+        Assume.assumeTrue(isSSHAvailable());
+
+        Set<User> users = provider.getUsers();
+
+        // This shows that we don't have any users matching the exclude regex, which is likely because those users
+        // exist but were excluded:
+        for (User user : users) {
+            Assert.assertFalse(user.getIdentifier().startsWith("s"));
+        }
+
+        Assert.assertNotNull(users);
+        Assert.assertTrue(users.size() > 0);
+    }
+
+    /**
+     * Tests the provider behavior by getting a known user by id.
+     *
+     * @param provider {@link UserGroupProvider}
+     */
+    private void verifyKnownUserByUsername(UserGroupProvider provider) {
+        Assume.assumeTrue(isSSHAvailable());
+
+        User root = provider.getUserByIdentity(KNOWN_USER);
+        Assert.assertNotNull(root);
+        Assert.assertEquals(KNOWN_USER, root.getIdentity());
+    }
+
+    /**
+     * Tests the provider behavior by getting its groups and checking minimum size.
+     *
+     * @param provider {@link UserGroupProvider}
+     */
+    private void verifyGroupsAndMinimumGroupCount(UserGroupProvider provider) {
+        Assume.assumeTrue(isSSHAvailable());
+
+        Set<Group> groups = provider.getGroups();
+
+        // This shows that we don't have any groups matching the exclude regex, which is likely because those groups
+        // exist but were excluded:
+        for (Group group : groups) {
+            Assert.assertFalse(group.getName().endsWith("d"));
+        }
+
+        Assert.assertNotNull(groups);
+        Assert.assertTrue(groups.size() > 0);
+    }
+
+    /**
+     * Tests the provider behavior by getting a known user and checking its group membership.
+     *
+     * @param provider {@link UserGroupProvider}
+     */
+    private void verifyGetUserByIdentityAndGetGroupMembership(UserGroupProvider provider) {
+        Assume.assumeTrue(isSSHAvailable());
+
+        UserAndGroups user = provider.getUserAndGroups(KNOWN_USER);
+        Assert.assertNotNull(user);
+
+        try {
+            Assert.assertTrue(user.getGroups().size() > 0);
+            logger.info("root user group count: " + user.getGroups().size());
+        } catch (final AssertionError ignored) {
+            logger.info("root user and groups group count zero on this system");
+        }
+
+        Set<Group> groups = provider.getGroups();
+        Assert.assertTrue(groups.size() > user.getGroups().size());
+    }
 }