diff --git a/reviewboard/accounts/decorators.py b/reviewboard/accounts/decorators.py
index abba8300166c1b0fd2ccb3fbefa1d86708dc43e6..c943b7a97e6648405f3c52808122eb2c5b7c066d 100644
--- a/reviewboard/accounts/decorators.py
+++ b/reviewboard/accounts/decorators.py
@@ -37,17 +37,18 @@ def valid_prefs_required(view_func):
     def _check_valid_prefs(request, *args, **kwargs):
         # Fetch the profile. If it exists, we're done, and it's cached for
         # later. If not, try to create it.
-        try:
-            request.user.get_profile()
-        except Profile.DoesNotExist:
-            # Inbetween the request and now, the profile may have been
-            # created. That's okay, because we don't have anything special
-            # to set, so just ignore it.
+        if request.user.is_authenticated():
             try:
-                Profile.objects.create(user=request.user)
-            except IntegrityError:
-                # It was created already. We're satisfied, so bail.
-                pass
+                request.user.get_profile()
+            except Profile.DoesNotExist:
+                # Inbetween the request and now, the profile may have been
+                # created. That's okay, because we don't have anything special
+                # to set, so just ignore it.
+                try:
+                    Profile.objects.create(user=request.user)
+                except IntegrityError:
+                    # It was created already. We're satisfied, so bail.
+                    pass
 
         return view_func(request, *args, **kwargs)
 
diff --git a/reviewboard/reviews/views.py b/reviewboard/reviews/views.py
index 4745b2967c989bee984198cb7e2a6a11c4b2a98b..2237e63a3b14edf36637b79bf3e75849e2de8e95 100644
--- a/reviewboard/reviews/views.py
+++ b/reviewboard/reviews/views.py
@@ -55,7 +55,8 @@ from reviewboard.reviews.models import Comment, \
                                        Screenshot, ScreenshotComment
 from reviewboard.scmtools.core import PRE_CREATION
 from reviewboard.scmtools.errors import SCMError
-from reviewboard.site.models import LocalSite
+from reviewboard.site.decorators import check_local_site_access
+from reviewboard.site.urlresolvers import local_site_reverse
 from reviewboard.ssh.errors import SSHError
 from reviewboard.webapi.encoder import status_to_string
 
@@ -75,7 +76,7 @@ def _render_permission_denied(
     return response
 
 
-def _find_review_request(request, review_request_id, local_site_name):
+def _find_review_request(request, review_request_id, local_site):
     """
     Find a review request based on an ID, optional LocalSite name and optional
     select related query.
@@ -88,11 +89,7 @@ def _find_review_request(request, review_request_id, local_site_name):
     """
     q = ReviewRequest.objects.all()
 
-    if local_site_name:
-        local_site = get_object_or_404(LocalSite, name=local_site_name)
-        if not local_site.is_accessible_by(request.user):
-            return None, _render_permission_denied(request)
-
+    if local_site:
         q = q.filter(local_site=local_site,
                      local_id=review_request_id)
     else:
@@ -220,22 +217,36 @@ fields_changed_name_map = {
 ##### View functions
 #####
 
+@check_login_required
+@valid_prefs_required
+def root(request, local_site_name=None):
+    """Handles the root URL of Review Board or a Local Site.
+
+    If the user is authenticated, this will redirect to their Dashboard.
+    Otherwise, they'll be redirected to the All Review Requests page.
+
+    Either page may then redirect for login or show a Permission Denied,
+    depending on the settings.
+    """
+    if request.user.is_authenticated():
+        url_name = 'dashboard'
+    else:
+        url_name = 'all-review-requests'
+
+    return HttpResponseRedirect(
+        local_site_reverse(url_name, local_site_name=local_site_name))
+
+
 @login_required
+@check_local_site_access
 def new_review_request(request,
-                       local_site_name=None,
+                       local_site=None,
                        template_name='reviews/new_review_request.html'):
     """
     Displays a New Review Request form and handles the creation of a
     review request based on either an existing changeset or the provided
     information.
     """
-    if local_site_name:
-        local_site = get_object_or_404(LocalSite, name=local_site_name)
-        if not local_site.is_accessible_by(request.user):
-            return _render_permission_denied(request)
-    else:
-        local_site = None
-
     if request.method == 'POST':
         form = NewReviewRequestForm(request, request.user, local_site,
                                     request.POST, request.FILES)
@@ -260,9 +271,10 @@ def new_review_request(request,
 
 
 @check_login_required
+@check_local_site_access
 def review_detail(request,
                   review_request_id,
-                  local_site_name=None,
+                  local_site=None,
                   template_name="reviews/review_detail.html"):
     """
     Main view for review requests. This covers the review request information
@@ -273,7 +285,7 @@ def review_detail(request,
     # local_site configured to have its own review request ID namespace
     # starting from 1.
     review_request, response = _find_review_request(
-        request, review_request_id, local_site_name)
+        request, review_request_id, local_site)
 
     if not review_request:
         return response
@@ -712,13 +724,14 @@ def review_detail(request,
 
 
 @login_required
+@check_local_site_access
 @cache_control(no_cache=True, no_store=True, max_age=0, must_revalidate=True)
 def review_draft_inline_form(request,
                              review_request_id,
                              template_name,
-                             local_site_name=None):
+                             local_site=None):
     review_request, response = \
-        _find_review_request(request, review_request_id, local_site_name)
+        _find_review_request(request, review_request_id, local_site)
 
     if not review_request:
         return response
@@ -738,18 +751,13 @@ def review_draft_inline_form(request,
 
 
 @check_login_required
+@check_local_site_access
 def all_review_requests(request,
-                        local_site_name=None,
+                        local_site=None,
                         template_name='reviews/datagrid.html'):
     """
     Displays a list of all review requests.
     """
-    if local_site_name:
-        local_site = get_object_or_404(LocalSite, name=local_site_name)
-        if not local_site.is_accessible_by(request.user):
-            return _render_permission_denied(request)
-    else:
-        local_site = None
     datagrid = ReviewRequestDataGrid(
         request,
         ReviewRequest.objects.public(user=request.user,
@@ -762,44 +770,35 @@ def all_review_requests(request,
 
 
 @check_login_required
+@check_local_site_access
 def submitter_list(request,
-                   local_site_name=None,
+                   local_site=None,
                    template_name='reviews/datagrid.html'):
     """
     Displays a list of all users.
     """
-    if local_site_name:
-        local_site = get_object_or_404(LocalSite, name=local_site_name)
-        if not local_site.is_accessible_by(request.user):
-            return _render_permission_denied(request)
-    else:
-        local_site = None
     grid = SubmitterDataGrid(request, local_site=local_site)
     return grid.render_to_response(template_name)
 
 
 @check_login_required
+@check_local_site_access
 def group_list(request,
-               local_site_name=None,
+               local_site=None,
                template_name='reviews/datagrid.html'):
     """
     Displays a list of all review groups.
     """
-    if local_site_name:
-        local_site = get_object_or_404(LocalSite, name=local_site_name)
-        if not local_site.is_accessible_by(request.user):
-            return _render_permission_denied(request)
-    else:
-        local_site = None
     grid = GroupDataGrid(request, local_site=local_site)
     return grid.render_to_response(template_name)
 
 
 @login_required
+@check_local_site_access
 @valid_prefs_required
 def dashboard(request,
               template_name='reviews/dashboard.html',
-              local_site_name=None):
+              local_site=None):
     """
     The dashboard view, showing review requests organized by a variety of
     lists, depending on the 'view' parameter.
@@ -817,13 +816,6 @@ def dashboard(request,
     view = request.GET.get('view', None)
     context = {}
 
-    if local_site_name:
-        local_site = get_object_or_404(LocalSite, name=local_site_name)
-        if not local_site.is_accessible_by(request.user):
-            return _render_permission_denied(request)
-    else:
-        local_site = None
-
     if view == "watched-groups":
         # This is special. We want to return a list of groups, not
         # review requests.
@@ -841,20 +833,15 @@ def dashboard(request,
 
 
 @check_login_required
+@check_local_site_access
 def group(request,
           name,
           template_name='reviews/datagrid.html',
-          local_site_name=None):
+          local_site=None):
     """
     A list of review requests belonging to a particular group.
     """
     # Make sure the group exists
-    if local_site_name:
-        local_site = get_object_or_404(LocalSite, name=local_site_name)
-        if not local_site.is_accessible_by(request.user):
-            return _render_permission_denied(request)
-    else:
-        local_site = None
     group = get_object_or_404(Group, name=name, local_site=local_site)
 
     if not group.is_accessible_by(request.user):
@@ -874,20 +861,14 @@ def group(request,
 
 
 @check_login_required
+@check_local_site_access
 def group_members(request,
                   name,
                   template_name='reviews/datagrid.html',
-                  local_site_name=None):
+                  local_site=None):
     """
     A list of users registered for a particular group.
     """
-    if local_site_name:
-        local_site = get_object_or_404(LocalSite, name=local_site_name)
-        if not local_site.is_accessible_by(request.user):
-            return _render_permission_denied(request)
-    else:
-        local_site = None
-
     # Make sure the group exists
     group = get_object_or_404(Group,
                               name=name,
@@ -905,20 +886,14 @@ def group_members(request,
 
 
 @check_login_required
+@check_local_site_access
 def submitter(request,
               username,
               template_name='reviews/user_page.html',
-              local_site_name=None):
+              local_site=None):
     """
     A list of review requests owned by a particular user.
     """
-    if local_site_name:
-        local_site = get_object_or_404(LocalSite, name=local_site_name)
-        if not local_site.is_accessible_by(request.user):
-            return _render_permission_denied(request)
-    else:
-        local_site = None
-
     # Make sure the user exists
     if local_site:
         try:
@@ -946,11 +921,12 @@ def submitter(request,
 
 
 @check_login_required
+@check_local_site_access
 def diff(request,
          review_request_id,
          revision=None,
          interdiff_revision=None,
-         local_site_name=None,
+         local_site=None,
          template_name='diffviewer/view_diff.html'):
     """
     A wrapper around diffviewer.views.view_diff that handles querying for
@@ -958,7 +934,7 @@ def diff(request,
     providing the user's current review of the diff if it exists.
     """
     review_request, response = \
-        _find_review_request(request, review_request_id, local_site_name)
+        _find_review_request(request, review_request_id, local_site)
 
     if not review_request:
         return response
@@ -1058,16 +1034,14 @@ def diff(request,
 
 
 @check_login_required
-def raw_diff(request,
-             review_request_id,
-             revision=None,
-             local_site_name=None):
+@check_local_site_access
+def raw_diff(request, review_request_id, revision=None, local_site=None):
     """
     Displays a raw diff of all the filediffs in a diffset for the
     given review request.
     """
     review_request, response = \
-        _find_review_request(request, review_request_id, local_site_name)
+        _find_review_request(request, review_request_id, local_site)
 
     if not review_request:
         return response
@@ -1092,6 +1066,7 @@ def raw_diff(request,
 
 
 @check_login_required
+@check_local_site_access
 def comment_diff_fragments(
     request,
     review_request_id,
@@ -1099,7 +1074,7 @@ def comment_diff_fragments(
     template_name='reviews/load_diff_comment_fragments.js',
     comment_template_name='reviews/diff_comment_fragment.html',
     error_template_name='diffviewer/diff_fragment_error.html',
-    local_site_name=None):
+    local_site=None):
     """
     Returns the fragment representing the parts of a diff referenced by the
     specified list of comment IDs. This is used to allow batch lazy-loading
@@ -1109,7 +1084,7 @@ def comment_diff_fragments(
     # While we don't actually need the review request, we still want to do this
     # lookup in order to get the permissions checking.
     review_request, response = \
-        _find_review_request(request, review_request_id, local_site_name)
+        _find_review_request(request, review_request_id, local_site)
 
     if not review_request:
         return response
@@ -1145,6 +1120,7 @@ def comment_diff_fragments(
 
 
 @check_login_required
+@check_local_site_access
 def diff_fragment(request,
                   review_request_id,
                   revision,
@@ -1152,7 +1128,7 @@ def diff_fragment(request,
                   interdiff_revision=None,
                   chunkindex=None,
                   template_name='diffviewer/diff_file_fragment.html',
-                  local_site_name=None):
+                  local_site=None):
     """
     Wrapper around diffviewer.views.view_diff_fragment that takes a review
     request.
@@ -1162,7 +1138,7 @@ def diff_fragment(request,
     diff.
     """
     review_request, response = \
-        _find_review_request(request, review_request_id, local_site_name)
+        _find_review_request(request, review_request_id, local_site)
 
     if not review_request:
         return response
@@ -1183,6 +1159,7 @@ def diff_fragment(request,
 
 
 @check_login_required
+@check_local_site_access
 def preview_review_request_email(
     request,
     review_request_id,
@@ -1190,7 +1167,7 @@ def preview_review_request_email(
     text_template_name='notifications/review_request_email.txt',
     html_template_name='notifications/review_request_email.html',
     changedesc_id=None,
-    local_site_name=None):
+    local_site=None):
     """
     Previews the e-mail message that would be sent for an initial
     review request or an update.
@@ -1198,7 +1175,7 @@ def preview_review_request_email(
     This is mainly used for debugging.
     """
     review_request, response = \
-        _find_review_request(request, review_request_id, local_site_name)
+        _find_review_request(request, review_request_id, local_site)
 
     if not review_request:
         return response
@@ -1232,11 +1209,12 @@ def preview_review_request_email(
 
 
 @check_login_required
+@check_local_site_access
 def preview_review_email(request, review_request_id, review_id, format,
                          text_template_name='notifications/review_email.txt',
                          html_template_name='notifications/review_email.html',
                          extra_context={},
-                         local_site_name=None):
+                         local_site=None):
     """
     Previews the e-mail message that would be sent for a review of a
     review request.
@@ -1244,7 +1222,7 @@ def preview_review_email(request, review_request_id, review_id, format,
     This is mainly used for debugging.
     """
     review_request, response = \
-        _find_review_request(request, review_request_id, local_site_name)
+        _find_review_request(request, review_request_id, local_site)
 
     if not review_request:
         return response
@@ -1285,11 +1263,12 @@ def preview_review_email(request, review_request_id, review_id, format,
 
 
 @check_login_required
+@check_local_site_access
 def preview_reply_email(request, review_request_id, review_id, reply_id,
                         format,
                         text_template_name='notifications/reply_email.txt',
                         html_template_name='notifications/reply_email.html',
-                        local_site_name=None):
+                        local_site=None):
     """
     Previews the e-mail message that would be sent for a reply to a
     review of a review request.
@@ -1297,7 +1276,7 @@ def preview_reply_email(request, review_request_id, review_id, reply_id,
     This is mainly used for debugging.
     """
     review_request, response = \
-        _find_review_request(request, review_request_id, local_site_name)
+        _find_review_request(request, review_request_id, local_site)
 
     if not review_request:
         return response
@@ -1339,13 +1318,12 @@ def preview_reply_email(request, review_request_id, review_id, reply_id,
 
 
 @check_login_required
-def review_file_attachment(request,
-                           review_request_id,
-                           file_attachment_id,
-                           local_site_name=None):
+@check_local_site_access
+def review_file_attachment(request, review_request_id, file_attachment_id,
+                           local_site=None):
     """Displays a file attachment with a review UI."""
     review_request, response = \
-        _find_review_request(request, review_request_id, local_site_name)
+        _find_review_request(request, review_request_id, local_site)
 
     if not review_request:
         return response
@@ -1360,15 +1338,14 @@ def review_file_attachment(request,
 
 
 @check_login_required
-def view_screenshot(request,
-                    review_request_id,
-                    screenshot_id,
-                    local_site_name=None):
+@check_local_site_access
+def view_screenshot(request, review_request_id, screenshot_id,
+                    local_site=None):
     """
     Displays a screenshot, along with any comments that were made on it.
     """
     review_request, response = \
-        _find_review_request(request, review_request_id, local_site_name)
+        _find_review_request(request, review_request_id, local_site)
 
     if not review_request:
         return response
@@ -1380,9 +1357,10 @@ def view_screenshot(request,
 
 
 @check_login_required
+@check_local_site_access
 def search(request,
            template_name='reviews/search.html',
-           local_site_name=None):
+           local_site=None):
     """
     Searches review requests on Review Board based on a query string.
     """
@@ -1440,7 +1418,7 @@ def search(request,
     searcher.close()
 
     results = ReviewRequest.objects.filter(id__in=result_ids,
-                                           local_site__name=local_site_name)
+                                           local_site=local_site)
 
     return object_list(request=request,
                        queryset=results,
@@ -1452,22 +1430,16 @@ def search(request,
 
 
 @check_login_required
+@check_local_site_access
 def user_infobox(request, username,
                  template_name='accounts/user_infobox.html',
-                 local_site_name=None):
+                 local_site=None):
     """Displays a user info popup.
 
     This is meant to be embedded in other pages, rather than being
     a standalone page.
     """
     user = get_object_or_404(User, username=username)
-
-    if local_site_name:
-        local_site = get_object_or_404(LocalSite, name=local_site_name)
-
-        if not local_site.is_accessible_by(request.user):
-            return _render_permission_denied(request)
-
     show_profile = user.is_profile_visible(request.user)
 
     etag = ':'.join([user.first_name.encode('ascii', 'replace'),
diff --git a/reviewboard/site/decorators.py b/reviewboard/site/decorators.py
new file mode 100644
index 0000000000000000000000000000000000000000..517bc9819a76ef8c6f16b3943f71e5402e88e8d7
--- /dev/null
+++ b/reviewboard/site/decorators.py
@@ -0,0 +1,60 @@
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect
+from django.shortcuts import get_object_or_404, render_to_response
+from django.template.context import RequestContext
+from djblets.util.decorators import simple_decorator
+
+from reviewboard.site.models import LocalSite
+
+
+@simple_decorator
+def check_local_site_access(view_func):
+    """Checks if a user has access to a Local Site.
+
+    This checks whether or not the logged-in user is either a member of
+    a Local Site or if the user otherwise has access to it.
+    given local site. If not, this shows a permission denied page.
+    """
+    def _check(request, local_site_name=None, *args, **kwargs):
+        if local_site_name:
+            local_site = get_object_or_404(LocalSite, name=local_site_name)
+
+            if not local_site.is_accessible_by(request.user):
+                if local_site.public or request.user.is_authenticated():
+                    response = render_to_response('permission_denied.html',
+                                                  RequestContext(request))
+                    response.status_code = 403
+                    return response
+                else:
+                    return HttpResponseRedirect(
+                        '%s?next_page=%s'
+                        % (reverse('login'), request.get_full_path()))
+        else:
+            local_site = None
+
+        return view_func(request, local_site=local_site, *args, **kwargs)
+
+    return _check
+
+
+@simple_decorator
+def check_localsite_admin(view_func):
+    """Checks if a user is an admin on a Local Site.
+
+    This checks whether or not the logged-in user is marked as an admin for the
+    given local site. If not, this shows a permission denied page.
+    """
+    def _check(request, local_site_name=None, *args, **kwargs):
+        if local_site_name:
+            site = get_object_or_404(LocalSite, name=local_site_name)
+
+            if not site.is_mutable_by(request.user):
+                response = render_to_response('permission_denied.html',
+                                              RequestContext(request))
+                response.status_code = 403
+                return response
+
+        return view_func(request, local_site_name=local_site_name,
+                         *args, **kwargs)
+
+    return _check
diff --git a/reviewboard/site/models.py b/reviewboard/site/models.py
index ab8bd12e34e10f324659c986ce6842888f9d4f50..f9164846abe0b150385ea963def213d136da5146 100644
--- a/reviewboard/site/models.py
+++ b/reviewboard/site/models.py
@@ -58,9 +58,9 @@ class LocalSite(models.Model):
         This checks that the user is logged in, and that they're listed in the
         'users' field.
         """
-        return (user.is_authenticated() and
-                (user.is_staff or self.public or
-                 self.users.filter(pk=user.pk).exists()))
+        return (self.public or
+                (user.is_authenticated() and
+                 (user.is_staff or self.users.filter(pk=user.pk).exists())))
 
     def is_mutable_by(self, user, perm='site.change_localsite'):
         """Returns whether or not a user can modify settings in a LocalSite.
diff --git a/reviewboard/templates/permission_denied.html b/reviewboard/templates/permission_denied.html
new file mode 100644
index 0000000000000000000000000000000000000000..d0c78e5e293728d37cb66c3b6b8c333d52ed02ca
--- /dev/null
+++ b/reviewboard/templates/permission_denied.html
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+{% load djblets_deco i18n %}
+
+{% block title %}{% trans "Permission Denied" %}{% endblock %}
+
+{% block content %}
+ <div class="permissions-offset">
+{%  box "important" %}
+  <h1>{% trans "Permission Denied" %}</h1>
+  <p>{% trans "You do not have permission to access this page." %}</p>
+{%  endbox %}
+ </div>
+{% endblock %}
diff --git a/reviewboard/urls.py b/reviewboard/urls.py
index 2c5901a5881bc6322380a7bd27083c7a8e39a354..df9ec8eead6c3c439f886109a31d8610186bee8f 100644
--- a/reviewboard/urls.py
+++ b/reviewboard/urls.py
@@ -71,9 +71,7 @@ if settings.DEBUG or getattr(settings, 'RUNNING_TEST', False):
     )
 
 localsite_urlpatterns = patterns('',
-    url(r'^$', 'django.views.generic.simple.redirect_to',
-        {'url': 'dashboard/'},
-        name="root"),
+    url(r'^$', 'reviewboard.reviews.views.root', name="root"),
 
     (r'^api/', include(root_resource.get_url_patterns())),
     (r'^r/', include('reviewboard.reviews.urls')),
