]> git.ipfire.org Git - thirdparty/squid.git/commitdiff
Windows port: Added new mswin_check_ad_group external ACL helper
authorGuido Serassio <serassio@squid-cache.org>
Sat, 17 May 2008 11:27:47 +0000 (13:27 +0200)
committerGuido Serassio <serassio@squid-cache.org>
Sat, 17 May 2008 11:27:47 +0000 (13:27 +0200)
This helper allow the lookup of users's group membership in a Windows
Active Directory domain.
It overcomes the Lan Manager limits of mswin_check_lm_group, but it can be
used only with native Windows Active Directory domains, so mswin_check_lm_group
will not removed from Squid.

configure.in [changed mode: 0644->0755]
helpers/external_acl/Makefile.am [changed mode: 0644->0755]
helpers/external_acl/mswin_ad_group/Makefile.am [new file with mode: 0644]
helpers/external_acl/mswin_ad_group/mswin_check_ad_group.c [new file with mode: 0644]
helpers/external_acl/mswin_ad_group/mswin_check_ad_group.h [new file with mode: 0644]
helpers/external_acl/mswin_ad_group/readme.txt [new file with mode: 0644]

old mode 100644 (file)
new mode 100755 (executable)
index c44185d..fb69fb2
@@ -3653,6 +3653,7 @@ AC_CONFIG_FILES([\
        helpers/external_acl/session/Makefile \
        helpers/external_acl/unix_group/Makefile \
        helpers/external_acl/wbinfo_group/Makefile \
+       helpers/external_acl/mswin_ad_group/Makefile \
        helpers/external_acl/mswin_lm_group/Makefile \
        tools/Makefile
 ])
old mode 100644 (file)
new mode 100755 (executable)
index f0cb571..94550ed
@@ -3,5 +3,5 @@
 #  $Id: Makefile.am,v 1.6 2006/03/19 01:30:30 hno Exp $
 #
 
-DIST_SUBDIRS   = ip_user ldap_group session unix_group wbinfo_group mswin_lm_group
+DIST_SUBDIRS   = ip_user ldap_group mswin_ad_group mswin_lm_group session unix_group wbinfo_group
 SUBDIRS                = @EXTERNAL_ACL_HELPERS@
diff --git a/helpers/external_acl/mswin_ad_group/Makefile.am b/helpers/external_acl/mswin_ad_group/Makefile.am
new file mode 100644 (file)
index 0000000..3de5897
--- /dev/null
@@ -0,0 +1,19 @@
+#
+#  Makefile for the Squid Object Cache server
+#
+#  $Id: Makefile.am,v 1.1 2008/05/17 10:23:13 serassio Exp $
+#
+#  Uncomment and customize the following to suit your needs:
+#
+
+
+libexec_PROGRAMS = mswin_check_ad_group
+
+mswin_check_ad_group_SOURCES = mswin_check_ad_group.c mswin_check_ad_group.h
+
+INCLUDES = -I$(top_srcdir)/include -I$(top_srcdir)/src
+
+LDADD   = -L$(top_builddir)/lib -lmiscutil -lnetapi32 -ladvapi32 \
+          -lntdll $(XTRA_LIBS)
+
+EXTRA_DIST = readme.txt
diff --git a/helpers/external_acl/mswin_ad_group/mswin_check_ad_group.c b/helpers/external_acl/mswin_ad_group/mswin_check_ad_group.c
new file mode 100644 (file)
index 0000000..40f7eca
--- /dev/null
@@ -0,0 +1,485 @@
+/*
+ * mswin_check_ad_group: lookup group membership in a Windows
+ * Active Directory domain
+ *
+ * (C)2008 Guido Serassio - Acme Consulting S.r.l.
+ *
+ * Authors:
+ *  Guido Serassio <guido.serassio@acmeconsulting.it>
+ *  Acme Consulting S.r.l., Italy <http://www.acmeconsulting.it>
+ *
+ * With contributions from others mentioned in the change history section
+ * below.
+ *
+ * Based on mswin_check_lm_group by Guido Serassio.
+ *
+ * Dependencies: Windows 2000 SP4 and later.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
+ *
+ * History:
+ *
+ * Version 1.0
+ * 02-05-2008 Guido Serassio
+ *              First release, based on mswin_check_lm_group.
+ *
+ * This is a helper for the external ACL interface for Squid Cache
+ * 
+ * It reads from the standard input the domain username and a list of
+ * groups and tries to match it against the groups membership of the
+ * specified username.
+ *
+ * Returns `OK' if the user belongs to a group or `ERR' otherwise, as
+ * described on http://devel.squid-cache.org/external_acl/config.html
+ *
+ */
+
+#include "config.h"
+#ifdef _SQUID_CYGWIN_
+#include <wchar.h>
+int _wcsicmp(const wchar_t *, const wchar_t *);
+#endif
+#if HAVE_STDIO_H
+#include <stdio.h>
+#endif
+#if HAVE_CTYPE_H
+#include <ctype.h>
+#endif
+#ifdef HAVE_STRING_H
+#include <string.h>
+#endif
+#if HAVE_GETOPT_H
+#include <getopt.h>
+#endif
+#undef assert
+#include <assert.h>
+#include <windows.h>
+#include <lm.h>
+#include <dsgetdc.h>
+#include <dsrole.h>
+
+#include "util.h"
+
+#define BUFSIZE 8192           /* the stdin buffer size */
+int use_global = 0;
+char debug_enabled = 0;
+char *myname;
+pid_t mypid;
+char *machinedomain;
+int use_case_insensitive_compare = 0;
+char *DefaultDomain = NULL;
+const char NTV_VALID_DOMAIN_SEPARATOR[] = "\\/";
+
+#include "mswin_check_ad_group.h"
+
+
+char *
+GetDomainName(void)
+{
+    static char *DomainName = NULL;
+    PDSROLE_PRIMARY_DOMAIN_INFO_BASIC pDSRoleInfo;
+    DWORD netret;
+
+    if ((netret = DsRoleGetPrimaryDomainInformation(NULL, DsRolePrimaryDomainInfoBasic, (PBYTE *) & pDSRoleInfo) == ERROR_SUCCESS)) {
+       /* 
+        * Check the machine role.
+        */
+
+       if ((pDSRoleInfo->MachineRole == DsRole_RoleMemberWorkstation) ||
+           (pDSRoleInfo->MachineRole == DsRole_RoleMemberServer) ||
+           (pDSRoleInfo->MachineRole == DsRole_RoleBackupDomainController) ||
+           (pDSRoleInfo->MachineRole == DsRole_RolePrimaryDomainController)) {
+
+           size_t len = wcslen(pDSRoleInfo->DomainNameFlat);
+
+           /* allocate buffer for str + null termination */
+           safe_free(DomainName);
+           DomainName = (char *) xmalloc(len + 1);
+           if (DomainName == NULL)
+               return NULL;
+
+           /* copy unicode buffer */
+           WideCharToMultiByte(CP_ACP, 0, pDSRoleInfo->DomainNameFlat, -1, DomainName, len, NULL, NULL);
+
+           /* add null termination */
+           DomainName[len] = '\0';
+
+           /* 
+            * Member of a domain. Display it in debug mode.
+            */
+           debug("Member of Domain %s\n", DomainName);
+           debug("Into forest %S\n", pDSRoleInfo->DomainForestName);
+
+       } else {
+           debug("Not a Domain member\n");
+       }
+    } else
+       debug("DsRoleGetPrimaryDomainInformation Error: %ld\n", netret);
+
+    /*
+     * Free the allocated memory.
+     */
+    if (pDSRoleInfo != NULL)
+       DsRoleFreeMemory(pDSRoleInfo);
+
+    return DomainName;
+}
+
+/* returns 0 on match, -1 if no match */
+static int
+wcstrcmparray(const wchar_t * str, const char **array)
+{
+    WCHAR wszGroup[GNLEN + 1]; // Unicode Group
+
+    while (*array) {
+       MultiByteToWideChar(CP_ACP, 0, *array,
+           strlen(*array) + 1, wszGroup, sizeof(wszGroup) / sizeof(wszGroup[0]));
+       debug("Windows group: %S, Squid group: %S\n", str, wszGroup);
+       if ((use_case_insensitive_compare ? _wcsicmp(str, wszGroup) : wcscmp(str, wszGroup)) == 0)
+           return 0;
+       array++;
+    }
+    return -1;
+}
+
+/* returns 1 on success, 0 on failure */
+int
+Valid_Local_Groups(char *UserName, const char **Groups)
+{
+    int result = 0;
+    char *Domain_Separator;
+    WCHAR wszUserName[UNLEN + 1];      // Unicode user name
+
+    LPLOCALGROUP_USERS_INFO_0 pBuf;
+    LPLOCALGROUP_USERS_INFO_0 pTmpBuf;
+    DWORD dwLevel = 0;
+    DWORD dwFlags = LG_INCLUDE_INDIRECT;
+    DWORD dwPrefMaxLen = -1;
+    DWORD dwEntriesRead = 0;
+    DWORD dwTotalEntries = 0;
+    NET_API_STATUS nStatus;
+    DWORD i;
+    DWORD dwTotalCount = 0;
+    LPBYTE pBufTmp = NULL;
+
+    if ((Domain_Separator = strchr(UserName, '/')) != NULL)
+       *Domain_Separator = '\\';
+
+    debug("Valid_Local_Groups: checking group membership of '%s'.\n", UserName);
+
+/* Convert ANSI User Name and Group to Unicode */
+
+    MultiByteToWideChar(CP_ACP, 0, UserName,
+       strlen(UserName) + 1, wszUserName, sizeof(wszUserName) / sizeof(wszUserName[0]));
+
+    /*
+     * Call the NetUserGetLocalGroups function 
+     * specifying information level 0.
+     * 
+     * The LG_INCLUDE_INDIRECT flag specifies that the 
+     * function should also return the names of the local 
+     * groups in which the user is indirectly a member.
+     */
+    nStatus = NetUserGetLocalGroups(NULL,
+       wszUserName,
+       dwLevel,
+       dwFlags,
+       &pBufTmp,
+       dwPrefMaxLen,
+       &dwEntriesRead,
+       &dwTotalEntries);
+    pBuf = (LPLOCALGROUP_USERS_INFO_0) pBufTmp;
+    /*
+     * If the call succeeds,
+     */
+    if (nStatus == NERR_Success) {
+       if ((pTmpBuf = pBuf) != NULL) {
+           for (i = 0; i < dwEntriesRead; i++) {
+               assert(pTmpBuf != NULL);
+               if (pTmpBuf == NULL) {
+                   result = 0;
+                   break;
+               }
+               if (wcstrcmparray(pTmpBuf->lgrui0_name, Groups) == 0) {
+                   result = 1;
+                   break;
+               }
+               pTmpBuf++;
+               dwTotalCount++;
+           }
+       }
+    } else
+       result = 0;
+/*
+ * Free the allocated memory.
+ */
+    if (pBuf != NULL)
+       NetApiBufferFree(pBuf);
+    return result;
+}
+
+
+/* returns 1 on success, 0 on failure */
+int
+Valid_Global_Groups(char *UserName, const char **Groups)
+{
+    int result = 0;
+    WCHAR wszUserName[UNLEN + 1];      // Unicode user name
+
+    WCHAR wszDomainControllerName[UNCLEN + 1];
+
+    char NTDomain[DNLEN + UNLEN + 2];
+    char *domain_qualify = NULL;
+    char User[UNLEN + 1];
+    size_t j;
+
+    LPGROUP_USERS_INFO_0 pUsrBuf = NULL;
+    LPGROUP_USERS_INFO_0 pTmpBuf;
+    PDOMAIN_CONTROLLER_INFO pDCInfo = NULL;
+    DWORD dwLevel = 0;
+    DWORD dwPrefMaxLen = -1;
+    DWORD dwEntriesRead = 0;
+    DWORD dwTotalEntries = 0;
+    NET_API_STATUS nStatus;
+    DWORD i;
+    DWORD dwTotalCount = 0;
+    LPBYTE pBufTmp = NULL;
+
+    strncpy(NTDomain, UserName, sizeof(NTDomain));
+
+    for (j = 0; j < strlen(NTV_VALID_DOMAIN_SEPARATOR); j++) {
+       if ((domain_qualify = strchr(NTDomain, NTV_VALID_DOMAIN_SEPARATOR[j])) != NULL)
+           break;
+    }
+    if (domain_qualify == NULL) {
+       strcpy(User, NTDomain);
+       strcpy(NTDomain, DefaultDomain);
+    } else {
+       strcpy(User, domain_qualify + 1);
+       domain_qualify[0] = '\0';
+       strlwr(NTDomain);
+    }
+
+    debug("Valid_Global_Groups: checking group membership of '%s\\%s'.\n", NTDomain, User);
+
+    /* Convert ANSI User Name to Unicode */
+
+    MultiByteToWideChar(CP_ACP, 0, User,
+       strlen(User) + 1, wszUserName,
+       sizeof(wszUserName) / sizeof(wszUserName[0]));
+
+    /* Query AD for a DC */
+
+    if (DsGetDcName(NULL, NTDomain, NULL, NULL, DS_IS_FLAT_NAME | DS_RETURN_FLAT_NAME, &pDCInfo) != NO_ERROR) {
+       fprintf(stderr, "%s DsGetDcName() failed.'\n", myname);
+       if (pDCInfo != NULL)
+           NetApiBufferFree(pDCInfo);
+       return result;
+    }
+    /* Convert ANSI Domain Controller Name to Unicode */
+
+    MultiByteToWideChar(CP_ACP, 0, pDCInfo->DomainControllerName,
+       strlen(pDCInfo->DomainControllerName) + 1, wszDomainControllerName,
+       sizeof(wszDomainControllerName) / sizeof(wszDomainControllerName[0]));
+
+    debug("Using '%S' as DC for '%s' user's domain.\n", wszDomainControllerName, NTDomain);
+    debug("DC Active Directory Site is %s\n", pDCInfo->DcSiteName);
+    debug("Machine Active Directory Site is %s\n", pDCInfo->ClientSiteName);
+
+    /*
+     * Call the NetUserGetGroups function 
+     * specifying information level 0.
+     */
+    dwLevel = 0;
+    pBufTmp = NULL;
+    nStatus = NetUserGetGroups(wszDomainControllerName,
+       wszUserName,
+       dwLevel,
+       &pBufTmp,
+       dwPrefMaxLen,
+       &dwEntriesRead,
+       &dwTotalEntries);
+    pUsrBuf = (LPGROUP_USERS_INFO_0) pBufTmp;
+    /*
+     * If the call succeeds,
+     */
+    if (nStatus == NERR_Success) {
+       if ((pTmpBuf = pUsrBuf) != NULL) {
+           for (i = 0; i < dwEntriesRead; i++) {
+               assert(pTmpBuf != NULL);
+               if (pTmpBuf == NULL) {
+                   result = 0;
+                   break;
+               }
+               if (wcstrcmparray(pTmpBuf->grui0_name, Groups) == 0) {
+                   result = 1;
+                   break;
+               }
+               pTmpBuf++;
+               dwTotalCount++;
+           }
+       }
+    } else {
+       result = 0;
+       fprintf(stderr, "%s NetUserGetGroups() failed.'\n", myname);
+    }
+    /*
+     * Free the allocated memory.
+     */
+    if (pUsrBuf != NULL)
+       NetApiBufferFree(pUsrBuf);
+    if (pDCInfo != NULL)
+       NetApiBufferFree((LPVOID) pDCInfo);
+    return result;
+}
+
+static void
+usage(char *program)
+{
+    fprintf(stderr, "Usage: %s [-D domain][-G][-P][-c][-d][-h]\n"
+       " -D    default user Domain\n"
+       " -G    enable Domain Global group mode\n"
+       " -c    use case insensitive compare\n"
+       " -d    enable debugging\n"
+       " -h    this message\n",
+       program);
+}
+
+void
+process_options(int argc, char *argv[])
+{
+    int opt;
+
+    opterr = 0;
+    while (-1 != (opt = getopt(argc, argv, "D:Gcdh"))) {
+       switch (opt) {
+       case 'D':
+           DefaultDomain = xstrndup(optarg, DNLEN + 1);
+           strlwr(DefaultDomain);
+           break;
+       case 'G':
+           use_global = 1;
+           break;
+       case 'c':
+           use_case_insensitive_compare = 1;
+           break;
+       case 'd':
+           debug_enabled = 1;
+           break;
+       case 'h':
+           usage(argv[0]);
+           exit(0);
+       case '?':
+           opt = optopt;
+           /* fall thru to default */
+       default:
+           fprintf(stderr, "%s Unknown option: -%c. Exiting\n", myname, opt);
+           usage(argv[0]);
+           exit(1);
+           break;              /* not reached */
+       }
+    }
+    return;
+}
+
+
+int
+main(int argc, char *argv[])
+{
+    char *p;
+    char buf[BUFSIZE];
+    char *username;
+    char *group;
+    int err = 0;
+    const char *groups[512];
+    int n;
+
+    if (argc > 0) {            /* should always be true */
+       myname = strrchr(argv[0], '/');
+       if (myname == NULL)
+           myname = argv[0];
+    } else {
+       myname = "(unknown)";
+    }
+    mypid = getpid();
+
+    setbuf(stdout, NULL);
+    setbuf(stderr, NULL);
+
+    /* Check Command Line */
+    process_options(argc, argv);
+
+    if (use_global) {
+       if ((machinedomain = GetDomainName()) == NULL) {
+           fprintf(stderr, "%s Can't read machine domain\n", myname);
+           exit(1);
+       }
+       strlwr(machinedomain);
+       if (!DefaultDomain)
+           DefaultDomain = xstrdup(machinedomain);
+    }
+    debug("External ACL win32 group helper build " __DATE__ ", " __TIME__
+       " starting up...\n");
+    if (use_global)
+       debug("Domain Global group mode enabled using '%s' as default domain.\n", DefaultDomain);
+    if (use_case_insensitive_compare)
+       debug("Warning: running in case insensitive mode !!!\n");
+
+    /* Main Loop */
+    while (fgets(buf, sizeof(buf), stdin)) {
+       if (NULL == strchr(buf, '\n')) {
+           /* too large message received.. skip and deny */
+           fprintf(stderr, "%s: ERROR: Too large: %s\n", argv[0], buf);
+           while (fgets(buf, sizeof(buf), stdin)) {
+               fprintf(stderr, "%s: ERROR: Too large..: %s\n", argv[0], buf);
+               if (strchr(buf, '\n') != NULL)
+                   break;
+           }
+           goto error;
+       }
+       if ((p = strchr(buf, '\n')) != NULL)
+           *p = '\0';          /* strip \n */
+       if ((p = strchr(buf, '\r')) != NULL)
+           *p = '\0';          /* strip \r */
+
+       debug("Got '%s' from Squid (length: %d).\n", buf, strlen(buf));
+
+       if (buf[0] == '\0') {
+           fprintf(stderr, "Invalid Request\n");
+           goto error;
+       }
+       username = strtok(buf, " ");
+       for (n = 0; (group = strtok(NULL, " ")) != NULL; n++) {
+           rfc1738_unescape(group);
+           groups[n] = group;
+       }
+       groups[n] = NULL;
+
+       if (NULL == username) {
+           fprintf(stderr, "Invalid Request\n");
+           goto error;
+       }
+       rfc1738_unescape(username);
+
+       if ((use_global ? Valid_Global_Groups(username, groups) : Valid_Local_Groups(username, groups))) {
+           printf("OK\n");
+       } else {
+         error:
+           printf("ERR\n");
+       }
+       err = 0;
+    }
+    return 0;
+}
diff --git a/helpers/external_acl/mswin_ad_group/mswin_check_ad_group.h b/helpers/external_acl/mswin_ad_group/mswin_check_ad_group.h
new file mode 100644 (file)
index 0000000..b1778a1
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * (C) 2002, 2005 Guido Serassio <guido.serassio@acmeconsulting.it>
+ * Based on previous work of Francesco Chemolli, Robert Collins and Andrew Doran
+ *
+ * Distributed freely under the terms of the GNU General Public License,
+ * version 2. See the file COPYING for licensing details
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
+ */
+
+#undef debug
+
+/************* CONFIGURATION ***************/
+/*
+ * define this if you want debugging
+ */
+#ifndef DEBUG
+#define DEBUG
+#endif
+
+/************* END CONFIGURATION ***************/
+
+#include <sys/types.h>
+
+#define safe_free(x)   if (x) { free(x); x = NULL; }
+
+/* Debugging stuff */
+
+#ifdef __GNUC__                        /* this is really a gcc-ism */
+#ifdef DEBUG
+#include <stdio.h>
+#include <unistd.h>
+static char *__foo;
+extern char debug_enabled;
+#define debug(X...) if (debug_enabled) { \
+                    fprintf(stderr,"%s[%d](%s:%d): ", myname, mypid, \
+                    ((__foo=strrchr(__FILE__,'/'))==NULL?__FILE__:__foo+1),\
+                    __LINE__);\
+                    fprintf(stderr,X); }
+#else /* DEBUG */
+#define debug(X...)            /* */
+#endif /* DEBUG */
+#else /* __GNUC__ */
+extern char debug_enabled;
+static void
+debug(char *format,...)
+{
+#ifdef DEBUG
+#ifdef _SQUID_MSWIN_
+    if (debug_enabled) {
+       va_list args;
+
+       va_start(args, format);
+       fprintf(stderr, "%s[%d]: ", myname, mypid);
+       vfprintf(stderr, format, args);
+       fprintf(stderr, "\n");
+       va_end(args);
+    }
+#endif /* _SQUID_MSWIN_ */
+#endif /* DEBUG */
+}
+#endif /* __GNUC__ */
+
+
+/* A couple of harmless helper macros */
+#define SEND(X) debug("sending '%s' to squid\n",X); printf(X "\n");
+#ifdef __GNUC__
+#define SEND2(X,Y...) debug("sending '" X "' to squid\n",Y); printf(X "\n",Y);
+#else
+/* no gcc, no debugging. varargs macros are a gcc extension */
+#define SEND2(X,Y) debug("sending '" X "' to squid\n",Y); printf(X "\n",Y);
+#endif
diff --git a/helpers/external_acl/mswin_ad_group/readme.txt b/helpers/external_acl/mswin_ad_group/readme.txt
new file mode 100644 (file)
index 0000000..af8cc85
--- /dev/null
@@ -0,0 +1,83 @@
+
+This is the readme.txt file for mswin_check_ad_group, an external
+helper for the External ACL Scheme for Squid.
+
+
+This helper must be used in with an authentication scheme (tipically 
+basic, NTLM or Negotiate) based on Windows Active Directory domain users. 
+It reads from the standard input the domain username and a list of groups
+and tries to match it against the groups membership of the specified
+username.
+
+The minimal Windows version needed to run mswin_check_ad_group is
+a Windows 2000 SP4 member of an Active Directory Domain.
+
+==============
+Program Syntax
+==============
+
+mswin_check_lm_group [-D domain][-G][-c][-d][-h]
+
+-D domain specify the default user's domain
+-G        start helper in Domain Global Group mode
+-c        use case insensitive compare
+-d        enable debugging
+-h        this message
+
+
+================
+squid.conf usage
+================
+
+external_acl_type AD_global_group %LOGIN c:/squid/libexec/mswin_check_ad_group.exe -G
+external_acl_type NT_local_group %LOGIN c:/squid/libexec/mswin_check_ad_group.exe
+
+acl GProxyUsers external AD_global_group GProxyUsers
+acl LProxyUsers external NT_local_group LProxyUsers
+acl password proxy_auth REQUIRED
+
+http_access allow password GProxyUsers
+http_access allow password LProxyUsers
+http_access deny all
+
+In the previous example all validated AD users member of GProxyUsers Global 
+domain group or member of LProxyUsers machine local group are allowed to 
+use the cache.
+
+Groups with spaces in name, for example "Domain Users", must be quoted and
+the acl data ("Domain Users") must be placed into a separate file included
+by specifying "/path/to/file". The previous example will be:
+
+acl ProxyUsers external NT_global_group "c:/squid/etc/DomainUsers"
+
+and the DomainUsers files will contain only the following line:
+
+"Domain Users"
+
+NOTES: 
+- The standard group name comparison is case sensitive, so group name
+  must be specified with same case as in the Active Directory Domain.
+  It's possible to enable case insensitive group name comparison (-c),
+  but on some not-english locales, the results can be unexpected.
+- Native WIN32 NTLM and Basic Helpers must be used without the
+  -A & -D switches.
+
+Refer to Squid documentation for the more details on squid.conf.
+
+
+=======
+Testing
+=======
+
+I strongly reccomend that mswin_check_ad_group is tested prior to being used in a 
+production environment. It may behave differently on different platforms.
+To test it, run it from the command line. Enter username and group
+pairs separated by a space (username must entered with domain%5cusername
+syntax). Press ENTER to get an OK or ERR message.
+Make sure pressing <CTRL><D> behaves the same as a carriage return.
+Make sure pressing <CTRL><C> aborts the program.
+
+Test that entering no details does not result in an OK or ERR message.
+Test that entering an invalid username and group results in an ERR message.
+Test that entering an valid username and group results in an OK message.
+