Index: /trunk/reviewboard/accounts/backends.py
===================================================================
--- /trunk/reviewboard/accounts/backends.py	(revision 1670)
+++ /trunk/reviewboard/accounts/backends.py	(working copy)
@@ -141,3 +141,127 @@
 
     def get_user(self, user_id):
         return get_object_or_none(User, pk=user_id)
+
+
+class ActiveDirectoryBackend:
+    def get_domain_name(self):
+        return str(settings.AD_DOMAIN_NAME)
+
+    def get_ldap_search_root(self):
+        root = ['dc=%s' % x for x in self.get_domain_name().split('.')]
+        if settings.AD_OU_NAME:
+            root = ['ou=%s' % settings.AD_OU_NAME] + root
+        return ','.join(root)
+
+    def search_ad(self, con, filterstr):
+        import ldap
+        search_root = self.get_ldap_search_root()
+        logging.debug('Search root ' + search_root)
+        return con.search_s(search_root, scope=ldap.SCOPE_SUBTREE, filterstr=filterstr)
+
+    def find_domain_controllers_from_dns(self):
+        import DNS
+        DNS.Base.DiscoverNameServers()
+        q = '_ldap._tcp.%s' % self.get_domain_name()
+        req = DNS.Base.DnsRequest(q, qtype = 'SRV').req()
+        return [x['data'][-2:] for x in req.answers]
+
+    def can_recurse(self, depth):
+        return (settings.AD_RECURSION_DEPTH == -1 or
+                        depth <= settings.AD_RECURSION_DEPTH)
+
+    def get_member_of(self, con, search_results, seen=None, depth=0):
+        depth += 1
+        if seen is None:
+            seen = set()
+
+        for name, data in search_results:
+            if name is None:
+                continue
+            member_of = data.get('memberOf', [])
+            new_groups = [x.split(',')[0].split('=')[1] for x in member_of]
+            old_seen = seen.copy()
+            seen.update(new_groups)
+
+            # collect groups recursively
+            if self.can_recurse(depth):
+                for group in new_groups:
+                    if group in old_seen:
+                        continue
+                    group_data = self.search_ad(con, '(&(objectClass=group)(saMAccountName=%s))' % group)
+                    seen.update(self.get_member_of(con, group_data, seen=seen, depth=depth))
+            else:
+                logging.warning('ActiveDirectory recursive group check reached maximum recursion depth.')
+
+        return seen
+
+    def get_ldap_connections(self):
+        import ldap
+        if settings.AD_FIND_DC_FROM_DNS:
+            dcs = self.find_domain_controllers_from_dns()
+        else:
+            dcs = [('389', settings.AD_DOMAIN_CONTROLLER)]
+
+        for dc in dcs:
+            port, host = dc
+            con = ldap.open(host, port=int(port))
+            if settings.AD_USE_TLS:
+                con.start_tls_s()
+            con.set_option(ldap.OPT_REFERRALS, 0)
+            yield con
+
+    def authenticate(self, username, password):
+        import ldap
+        connections = self.get_ldap_connections()
+
+        for con in connections:
+            try:
+                bind_username ='%s@%s' % (username, self.get_domain_name())
+                con.simple_bind_s(bind_username, password)
+                user_data = self.search_ad(con, '(&(objectClass=user)(sAMAccountName=%s))' % username)
+                try:
+                    group_names = self.get_member_of(con, user_data)
+                except Exception, e:
+                    logging.error("Active Directory error: failed getting groups for user %s" % username)
+                    return None
+                required_group = settings.AD_GROUP_NAME
+                if required_group and not required_group in group_names:
+                    logging.warning("Active Directory: User %s is not in required group %s" % (username, required_group))
+                    return None
+
+                return self.get_or_create_user(username, user_data)
+            except ldap.SERVER_DOWN:
+                logging.warning('Active Directory: Domain controller is down - host: %s port: %s' % (host, port))
+                continue
+            except ldap.INVALID_CREDENTIALS:
+                logging.warning('Active Directory: Failed login for user %s' % username)
+                return None
+
+        logging.error('Active Directory error: Could not contact any domain controller servers')
+        return None
+
+    def get_or_create_user(self, username, ad_user_data):
+        try:
+            user = User.objects.get(username=username)
+            return user
+        except User.DoesNotExist:
+            try:
+                first_name = ad_user_data[0][1]['givenName'][0]
+                last_name = ad_user_data[0][1]['sn'][0]
+                email = u'%s@%s' % (username, settings.AD_DOMAIN_NAME)
+
+                user = User(username=username,
+                            password='',
+                            first_name=first_name,
+                            last_name=last_name,
+                            email=email)
+                user.is_staff = False
+                user.is_superuser = False
+                user.set_unusable_password()
+                user.save()
+                return user
+            except:
+                return None
+
+    def get_user(self, user_id):
+        return get_object_or_none(User, pk=user_id)
Index: /trunk/reviewboard/admin/checks.py
===================================================================
--- /trunk/reviewboard/admin/checks.py	(revision 1670)
+++ /trunk/reviewboard/admin/checks.py	(working copy)
@@ -116,3 +116,18 @@
             'LDAP authentication requires the python-ldap library, which '
             'is not installed.'
         ))
+
+def get_can_enable_dns():
+    """
+    Checks whether we can query DNS to find the domain controller to use.
+    """
+    try:
+        # XXX for reasons I don't understand imp.find_module doesn't work
+        #imp.find_module("DNS")
+        import DNS
+        return (True, None)
+    except ImportError:
+        return (False, _(
+            'PyDNS, which is required to find the domain controller, '
+            'is not installed.'
+            ))
Index: /trunk/reviewboard/admin/forms.py
===================================================================
--- /trunk/reviewboard/admin/forms.py	(revision 1670)
+++ /trunk/reviewboard/admin/forms.py	(working copy)
@@ -8,9 +8,11 @@
 from djblets.log import restart_logging
 from djblets.siteconfig.forms import SiteSettingsForm
 
-from reviewboard.admin.checks import get_can_enable_ldap, \
+from reviewboard.admin.checks import get_can_enable_dns, \
+                                     get_can_enable_ldap, \
                                      get_can_enable_search, \
                                      get_can_enable_syntax_highlighting
+
 from reviewboard.admin.siteconfig import load_site_config
 
 
@@ -62,6 +64,7 @@
         label=_("Authentication Method"),
         choices=(
             ("builtin", _("Standard registration")),
+            ("ad",      _("Active Directory")),
             ("ldap",    _("LDAP")),
             ("nis",     _("NIS")),
             ("custom",  _("Custom"))
@@ -122,6 +125,42 @@
         help_text=_("The optional password for the anonymous user."),
         required=False)
 
+    auth_ad_domain_name = forms.CharField(
+        label=_("Domain name"),
+        help_text=_("Enter the domain name to use, (ie. example.com). This will be "
+                    "used to query for LDAP servers and to bind to the domain."),
+        required=True)
+
+    auth_ad_use_tls = forms.BooleanField(
+        label=_("Use TLS for authentication"),
+        required=False)
+
+    auth_ad_find_dc_from_dns = forms.BooleanField(
+        label=_("Find DC from DNS"),
+        help_text=_("Query DNS to find which domain controller to use"),
+        required=False)
+
+    auth_ad_domain_controller = forms.CharField(
+        label=_("Domain controller"),
+        help_text=_("If not using DNS to find the DC specify the domain "
+                    "controller here"),
+        required=False)
+
+    auth_ad_ou_name = forms.CharField(
+        label=_("OU name"),
+        help_text=_("Optionally restrict users to specified OU."),
+        required=False)
+
+    auth_ad_group_name = forms.CharField(
+        label=_("Group name"),
+        help_text=_("Optionally restrict users to specified group."),
+        required=False)
+
+    auth_ad_recursion_depth = forms.IntegerField(
+        label=_("Recursion Depth"),
+        help_text=_("Depth to recurse when checking group membership. 0 to turn off, -1 for unlimited."),
+        required=False)
+
     custom_backends = forms.CharField(
         label=_("Backends"),
         help_text=_("A comma-separated list of custom auth backends. These "
@@ -146,7 +185,12 @@
             self.disabled_fields['search_index_file'] = True
             self.disabled_reasons['search_enable'] = _(reason)
 
+        can_enable_dns, reason = get_can_enable_dns()
+        if not can_enable_dns:
+            self.disabled_fields['auth_ad_find_dc_from_dns'] = _(reason)
+
         can_enable_ldap, reason = get_can_enable_ldap()
+
         if not can_enable_ldap:
             self.disabled_fields['auth_ldap_uri'] = True
             self.disabled_fields['auth_ldap_email_domain'] = True
@@ -156,6 +200,15 @@
             self.disabled_fields['auth_ldap_uid_mask'] = True
             self.disabled_fields['auth_ldap_anon_bind_uid'] = True
             self.disabled_fields['auth_ldap_anon_bind_password'] = True
+
+            self.disabled_fields['auth_ad_use_tls'] = True
+            self.disabled_fields['auth_ad_group_name'] = True
+            self.disabled_fields['auth_ad_recursion_depth'] = True
+            self.disabled_fields['auth_ad_ou_name'] = True
+            self.disabled_fields['auth_ad_find_dc_from_dns'] = True
+            self.disabled_fields['auth_ad_domain_controller'] = True
+            self.disabled_fields['auth_ad_domain_name'] = _(reason)
+
             self.disabled_reasons['auth_ldap_uri'] = _(reason)
 
         super(GeneralSettingsForm, self).load()
@@ -211,6 +264,9 @@
             if auth_backend != "nis":
                 set_fieldset_required("auth_nis", False)
 
+            if auth_backend != "ad":
+                set_fieldset_required("auth_ad", False)
+
             if auth_backend != "custom":
                 set_fieldset_required("auth_custom", False)
 
@@ -259,6 +315,19 @@
                             'auth_ldap_anon_bind_passwd'),
             },
             {
+                'id':      'auth_ad',
+                'classes': ('wide', 'hidden'),
+                'title':   _("Active Directory Authentication Settings"),
+                'fields':  ('auth_ad_domain_name',
+                            'auth_ad_use_tls',
+                            'auth_ad_find_dc_from_dns',
+                            'auth_ad_domain_controller',
+                            'auth_ad_ou_name',
+                            'auth_ad_group_name',
+                            'auth_ad_recursion_depth',
+                            ),
+            },
+            {
                 'id':      'auth_custom',
                 'classes': ('wide', 'hidden'),
                 'title':   _("Custom Authentication Settings"),
Index: /trunk/reviewboard/admin/siteconfig.py
===================================================================
--- /trunk/reviewboard/admin/siteconfig.py	(revision 1670)
+++ /trunk/reviewboard/admin/siteconfig.py	(working copy)
@@ -19,6 +19,7 @@
     'builtin': 'django.contrib.auth.backends.ModelBackend',
     'nis':     'reviewboard.accounts.backends.NISBackend',
     'ldap':    'reviewboard.accounts.backends.LDAPBackend',
+    'ad':      'reviewboard.accounts.backends.ActiveDirectoryBackend',
 }
 
 
@@ -33,6 +34,13 @@
     'auth_ldap_base_dn':          'LDAP_BASE_DN',
     'auth_ldap_uid_mask':         'LDAP_UID_MASK',
     'auth_ldap_uri':              'LDAP_URI',
+    'auth_ad_domain_name':        'AD_DOMAIN_NAME',
+    'auth_ad_use_tls':            'AD_USE_TLS',
+    'auth_ad_find_dc_from_dns':   'AD_FIND_DC_FROM_DNS',
+    'auth_ad_domain_controller':  'AD_DOMAIN_CONTROLLER',
+    'auth_ad_ou_name':            'AD_OU_NAME',
+    'auth_ad_group_name':         'AD_GROUP_NAME',
+    'auth_ad_recursion_depth':    'AD_RECURSION_DEPTH',
     'auth_nis_email_domain':      'NIS_EMAIL_DOMAIN',
     'site_domain_method':         'DOMAIN_METHOD',
 }
