diff --git a/lib/common/Makefile.am b/lib/common/Makefile.am
index d7aae53bfd..04d56dc3c8 100644
--- a/lib/common/Makefile.am
+++ b/lib/common/Makefile.am
@@ -1,108 +1,108 @@
 #
 # Copyright 2004-2022 the Pacemaker project contributors
 #
 # The version control history for this file may have further details.
 #
 # This source code is licensed under the GNU General Public License version 2
 # or later (GPLv2+) WITHOUT ANY WARRANTY.
 #
 include $(top_srcdir)/mk/common.mk
 
 AM_CPPFLAGS		+= -I$(top_builddir)/lib/gnu -I$(top_srcdir)/lib/gnu
 
 ## libraries
 lib_LTLIBRARIES	= libcrmcommon.la
 check_LTLIBRARIES = libcrmcommon_test.la
 
 # Disable -Wcast-qual if used, because we do some hacky casting,
 # and because libxml2 has some signatures that should be const but aren't
 # for backward compatibility reasons.
 
 # s390 needs -fPIC 
 # s390-suse-linux/bin/ld: .libs/ipc.o: relocation R_390_PC32DBL against `__stack_chk_fail@@GLIBC_2.4' can not be used when making a shared object; recompile with -fPIC
 
 CFLAGS		= $(CFLAGS_COPY:-Wcast-qual=) -fPIC
 
 # Without "." here, check-recursive will run through the subdirectories first
 # and then run "make check" here.  This will fail, because there's things in
 # the subdirectories that need check_LTLIBRARIES built first.  Adding "." here
 # changes the order so the subdirectories are processed afterwards.
 SUBDIRS = . tests
 
 noinst_HEADERS		= crmcommon_private.h mock_private.h
 
 libcrmcommon_la_LDFLAGS	= -version-info 43:0:9
 
 libcrmcommon_la_CFLAGS	= $(CFLAGS_HARDENED_LIB)
 libcrmcommon_la_LDFLAGS	+= $(LDFLAGS_HARDENED_LIB)
 
 libcrmcommon_la_LIBADD	= @LIBADD_DL@ $(top_builddir)/lib/gnu/libgnu.la
 
 # If configured with --with-profiling or --with-coverage, BUILD_PROFILING will
 # be set and -fno-builtin will be added to the CFLAGS.  However, libcrmcommon
 # uses the fabs() function which is normally supplied by gcc as one of its
 # builtins.  Therefore we need to explicitly link against libm here or the
 # tests won't link.
 if BUILD_PROFILING
 libcrmcommon_la_LIBADD	+= -lm
 endif
 
 # Use += rather than backlashed continuation lines for parsing by bumplibs
 libcrmcommon_la_SOURCES	=
 libcrmcommon_la_SOURCES	+= acl.c
 libcrmcommon_la_SOURCES	+= agents.c
 libcrmcommon_la_SOURCES	+= alerts.c
 libcrmcommon_la_SOURCES	+= attrd_client.c
 libcrmcommon_la_SOURCES	+= cib.c
 if BUILD_CIBSECRETS
 libcrmcommon_la_SOURCES	+= cib_secrets.c
 endif
 libcrmcommon_la_SOURCES	+= cmdline.c
 libcrmcommon_la_SOURCES	+= digest.c
 libcrmcommon_la_SOURCES	+= health.c
 libcrmcommon_la_SOURCES	+= io.c
 libcrmcommon_la_SOURCES	+= ipc_client.c
 libcrmcommon_la_SOURCES	+= ipc_common.c
 libcrmcommon_la_SOURCES	+= ipc_controld.c
 libcrmcommon_la_SOURCES	+= ipc_pacemakerd.c
 libcrmcommon_la_SOURCES 	+= ipc_schedulerd.c
 libcrmcommon_la_SOURCES	+= ipc_server.c
 libcrmcommon_la_SOURCES	+= iso8601.c
 libcrmcommon_la_SOURCES	+= lists.c
 libcrmcommon_la_SOURCES	+= logging.c
 libcrmcommon_la_SOURCES	+= mainloop.c
 libcrmcommon_la_SOURCES	+= messages.c
 libcrmcommon_la_SOURCES	+= nvpair.c
 libcrmcommon_la_SOURCES	+= operations.c
 libcrmcommon_la_SOURCES	+= options.c
 libcrmcommon_la_SOURCES	+= output.c
 libcrmcommon_la_SOURCES	+= output_html.c
 libcrmcommon_la_SOURCES	+= output_log.c
 libcrmcommon_la_SOURCES	+= output_none.c
 libcrmcommon_la_SOURCES	+= output_text.c
 libcrmcommon_la_SOURCES	+= output_xml.c
 libcrmcommon_la_SOURCES	+= patchset.c
 libcrmcommon_la_SOURCES	+= pid.c
 libcrmcommon_la_SOURCES	+= procfs.c
 libcrmcommon_la_SOURCES	+= remote.c
 libcrmcommon_la_SOURCES	+= results.c
 libcrmcommon_la_SOURCES	+= schemas.c
 libcrmcommon_la_SOURCES	+= scores.c
 libcrmcommon_la_SOURCES	+= strings.c
 libcrmcommon_la_SOURCES	+= utils.c
 libcrmcommon_la_SOURCES	+= watchdog.c
 libcrmcommon_la_SOURCES	+= xml.c
 libcrmcommon_la_SOURCES	+= xpath.c
 
-WRAPPED = calloc getenv getpwnam_r uname
+WRAPPED = calloc getenv getpwnam_r uname setgrent getgrent endgrent
 WRAPPED_FLAGS = $(foreach fn,$(WRAPPED),-Wl,--wrap=$(fn))
 
 libcrmcommon_test_la_SOURCES	= $(libcrmcommon_la_SOURCES)
 libcrmcommon_test_la_SOURCES	+= mock.c
 libcrmcommon_test_la_LDFLAGS	= $(LDFLAGS_HARDENED_LIB) $(WRAPPED_FLAGS)
 libcrmcommon_test_la_CFLAGS	= $(libcrmcommon_la_CFLAGS)
 libcrmcommon_test_la_LIBADD	= $(libcrmcommon_la_LIBADD)
 nodist_libcrmcommon_test_la_SOURCES = $(nodist_libcrmcommon_la_SOURCES)
 
 clean-generic:
 	rm -f *.log *.debug *.xml *~
diff --git a/lib/common/mock.c b/lib/common/mock.c
index 55812ddbc7..fa9431e6dc 100644
--- a/lib/common/mock.c
+++ b/lib/common/mock.c
@@ -1,76 +1,93 @@
 /*
  * Copyright 2021-2022 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #include <pwd.h>
 #include <stdlib.h>
 #include <sys/types.h>
 #include <sys/utsname.h>
+#include <grp.h>
 
 #include "mock_private.h"
 
 /* This file is only used when running "make check".  It is built into
  * libcrmcommon_test.a, not into libcrmcommon.so.  It is used to support
  * constructing mock versions of library functions for unit testing.
  *
- * Each unit test will only ever want to use a mocked version of one or two
- * library functions.  However, we need to mark all the mocked functions as
- * wrapped (with -Wl,--wrap= in the LDFLAGS) in libcrmcommon_test.a so that
- * all those unit tests can share the same special test library.  The unit
- * test then defines its own wrapped function.  Because a unit test won't
- * define every single wrapped function, there will be undefined references
- * at link time.
+ * Each unit test will only ever want to use a mocked version of a few
+ * library functions (i.e. not all of them). However, we need to mark all
+ * the mocked functions as wrapped (with -Wl,--wrap= in the LDFLAGS) in
+ * libcrmcommon_test.a so that all those unit tests can share the same
+ * special test library.  The unit test then defines its own wrapped
+ * function. Because a unit test won't define every single wrapped
+ * function, there will be undefined references at link time.
  *
  * This file takes care of those undefined references.  It defines a
  * wrapped version of every function that simply calls the real libc
  * version.  These wrapped versions are defined with a weak attribute,
  * which means the unit tests can define another wrapped version for
  * unit testing that will override the version defined here.
  *
  * HOW TO ADD A MOCKED FUNCTION:
  *
  * - Define a __wrap_X function here below with the same prototype as the
  *   actual function and that just calls __real_X.
  * - Add a __real_X and __wrap_X function prototype to mock_private.h.
  * - Add the function name to the WRAPPED variable in Makefile.am.
  *
  * HOW TO USE A MOCKED FUNCTION:
  *
  * - In the Makefile.am for your new test, add:
  *
  *   your_fn_test_LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la -lcmocka
  *   your_fn_test_LDFLAGS = -Wl,--wrap=X
  *
  *   You can use multiple wrapped functions by adding multiple -Wl
  *   arguments.
  * - #include "mock_private.h" in your test file.
  * - Add a __wrap_X function with the same prototype as the real function.
  * - Write your test cases, using will_return(), mock_type(), and
  *   mock_ptr_type() from cmocka.  See existing test cases for details.
  */
 
 void *__attribute__((weak))
 __wrap_calloc(size_t nmemb, size_t size) {
     return __real_calloc(nmemb, size);
 }
 
 char *__attribute__((weak))
 __wrap_getenv(const char *name) {
     return __real_getenv(name);
 }
 
 int __attribute__((weak))
 __wrap_getpwnam_r(const char *name, struct passwd *pwd,
                   char *buf, size_t buflen, struct passwd **result) {
     return __real_getpwnam_r(name, pwd, buf, buflen, result);
 }
 
 int __attribute__((weak))
 __wrap_uname(struct utsname *buf) {
     return __real_uname(buf);
 }
+
+void __attribute__((weak))
+__wrap_setgrent(void) {
+    __real_setgrent();
+}
+
+struct group * __attribute__((weak))
+__wrap_getgrent(void) {
+    return __real_getgrent();
+}
+
+void __attribute__((weak))
+__wrap_endgrent(void) {
+    __real_endgrent();
+}
+
diff --git a/lib/common/mock_private.h b/lib/common/mock_private.h
index 3df7c9839c..0c1134cc3a 100644
--- a/lib/common/mock_private.h
+++ b/lib/common/mock_private.h
@@ -1,34 +1,45 @@
 /*
  * Copyright 2021-2022 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef MOCK_PRIVATE__H
 #  define MOCK_PRIVATE__H
 
 #include <pwd.h>
 #include <stdlib.h>
 #include <sys/types.h>
 #include <sys/utsname.h>
+#include <grp.h>
 
 /* This header is for the sole use of libcrmcommon_test. */
 
 void *__real_calloc(size_t nmemb, size_t size);
 void *__wrap_calloc(size_t nmemb, size_t size);
 
 char *__real_getenv(const char *name);
 char *__wrap_getenv(const char *name);
 
 int __real_getpwnam_r(const char *name, struct passwd *pwd,
                       char *buf, size_t buflen, struct passwd **result);
 int __wrap_getpwnam_r(const char *name, struct passwd *pwd,
                       char *buf, size_t buflen, struct passwd **result);
 
 int __real_uname(struct utsname *buf);
 int __wrap_uname(struct utsname *buf);
 
+void __real_setgrent(void);
+void __wrap_setgrent(void);
+
+struct group *__real_getgrent(void);
+struct group *__wrap_getgrent(void);
+
+void __real_endgrent(void);
+void __wrap_endgrent(void);
+
+
 #endif  // MOCK_PRIVATE__H
diff --git a/lib/common/tests/acl/Makefile.am b/lib/common/tests/acl/Makefile.am
index 679c9cb8e6..a73fc354c5 100644
--- a/lib/common/tests/acl/Makefile.am
+++ b/lib/common/tests/acl/Makefile.am
@@ -1,21 +1,28 @@
 #
-# Copyright 2021 the Pacemaker project contributors
+# Copyright 2021-2022 the Pacemaker project contributors
 #
 # The version control history for this file may have further details.
 #
 # This source code is licensed under the GNU General Public License version 2
 # or later (GPLv2+) WITHOUT ANY WARRANTY.
 #
-AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include
+AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include -I$(top_srcdir)/lib/common
 LDADD = $(top_builddir)/lib/common/libcrmcommon.la -lcmocka
 
+pcmk__is_user_in_group_test_LDADD = $(top_builddir)/lib/common/libcrmcommon_test.la -lcmocka
+pcmk__is_user_in_group_test_LDFLAGS = \
+    -Wl,--wrap=setgrent \
+    -Wl,--wrap=getgrent \
+    -Wl,--wrap=endgrent
+
 include $(top_srcdir)/mk/tap.mk
 
 # Add "_test" to the end of all test program names to simplify .gitignore.
 
 check_PROGRAMS = \
+    pcmk__is_user_in_group_test \
 	pcmk_acl_required_test \
     xml_acl_denied_test \
     xml_acl_enabled_test
 
 TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/acl/pcmk__is_user_in_group_test.c b/lib/common/tests/acl/pcmk__is_user_in_group_test.c
new file mode 100644
index 0000000000..67b8c2c7c1
--- /dev/null
+++ b/lib/common/tests/acl/pcmk__is_user_in_group_test.c
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2020-2022 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU Lesser General Public License
+ * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+#include <crm/common/acl.h>
+#include "../../crmcommon_private.h"
+
+#include "mock_private.h"
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <setjmp.h>
+#include <cmocka.h>
+
+// THe index of the group that is going to be returned next from "get group entry" (getgrent)
+static int group_idx = 0;
+
+// Data used for testing
+static const char* grp0_members[] = {
+    "user0", "user1", NULL
+};
+
+static const char* grp1_members[] = {
+    "user1", NULL
+};
+
+static const char* grp2_members[] = {
+    "user2", "user1", NULL
+};
+
+// an array of "groups" (a struct from grp.h), the members of the groups are initalized here to some testing data.
+// Casting away the consts to make the compiler happy and simplify initialization. 
+// We never actually change these variables during the test!
+// string literal = const char* (cannot be changed b/c ? ) vs. char* (its getting casted to this)
+static const int NUM_GROUPS = 3;
+static struct group groups[] = {
+    {(char*)"grp0", (char*)"", 0, (char**)grp0_members},
+    {(char*)"grp1", (char*)"", 1, (char**)grp1_members},
+    {(char*)"grp2", (char*)"", 2, (char**)grp2_members},
+};
+
+// This function resets the group_idx to 0.
+void
+__wrap_setgrent(void) {
+    group_idx = 0;
+}
+
+// This function returns the next group entry in the list of groups, or
+// NULL if there aren't any left.
+// group_idx is a global variable which keeps track of where you are in the list
+struct group *
+__wrap_getgrent(void) {
+    if(group_idx >= NUM_GROUPS) return NULL;
+    return &groups[group_idx++];
+}
+
+void
+__wrap_endgrent(void) {
+}
+
+static void
+is_pcmk__is_user_in_group(void **state)
+{
+    // null user
+    assert_false(pcmk__is_user_in_group(NULL, "grp0"));
+    // null group
+    assert_false(pcmk__is_user_in_group("user0", NULL));
+    // nonexistent group
+    assert_false(pcmk__is_user_in_group("user0", "nonexistent_group"));
+    // user is in group
+    assert_true(pcmk__is_user_in_group("user0", "grp0"));
+    // user is not in group
+    assert_false(pcmk__is_user_in_group("user2", "grp0"));
+}
+
+int
+main(int argc, char **argv)
+{
+    const struct CMUnitTest tests[] = {
+        cmocka_unit_test(is_pcmk__is_user_in_group)
+    };
+
+    cmocka_set_message_output(CM_OUTPUT_TAP);
+    return cmocka_run_group_tests(tests, NULL, NULL);
+}