diff --git a/reviewboard/webapi/base.py b/reviewboard/webapi/base.py
index d9b8428f9d6e086ebc9203c66720f87f93bab31c..dd8e1cc2b8eb3f596478b2c7589ba198f5c35e96 100644
--- a/reviewboard/webapi/base.py
+++ b/reviewboard/webapi/base.py
@@ -26,9 +26,30 @@ class WebAPIResource(DjbletsWebAPIResource):
 
     mimetype_vendor = 'reviewboard.org'
 
+    @property
+    def policy_id(self):
+        """Returns the ID used for access policies.
+
+        This defaults to the name of the resource, but can be overridden
+        in case the name is not specific enough or there's a conflict.
+        """
+        return self.name
+
     def call_method_view(self, request, method, view, *args, **kwargs):
         # This will associate the token, if any, with the request.
-        self._get_api_token_for_request(request)
+        webapi_token = self._get_api_token_for_request(request)
+
+        if webapi_token:
+            policy = webapi_token.policy
+            resources_policy = policy.get('resources')
+
+            if resources_policy:
+                resource_id = kwargs.get(self.uri_object_key)
+
+                if not self.is_resource_method_allowed(resources_policy,
+                                                       method, resource_id):
+                    # The token's policies disallow access to this resource.
+                    return PERMISSION_DENIED
 
         return view(request, *args, **kwargs)
 
@@ -155,6 +176,77 @@ class WebAPIResource(DjbletsWebAPIResource):
 
         return q
 
+    def is_resource_method_allowed(self, resources_policy, method,
+                                   resource_id):
+        """Returns whether a method can be performed on a resource.
+
+        A method can be performed if a specific per-resource policy allows
+        it, and the global policy also allows it.
+
+        The per-resource policy takes precedence over the global policy.
+        If, for instance, the global policy blocks and the resource policies
+        allows, the method will be allowed.
+
+        If no policies apply to this, then the default is to allow.
+        """
+        # First check the resource policy. For this, we'll want to look in
+        # both the resource ID and the '*' wildcard.
+        resource_policy = resources_policy.get(self.policy_id)
+
+        if resource_policy:
+            permission = self._check_resource_policy(
+                resource_policy, method, [resource_id, '*'])
+
+            if permission is not None:
+                return permission
+
+        # Nothing was found there. Now check in the global policy. Note that
+        # there isn't a sub-key of 'resources.*', so we'll check based on
+        # resources_policy.
+        if '*' in resources_policy:
+            permission = self._check_resource_policy(
+                resources_policy, method, ['*'])
+
+            if permission is not None:
+                return permission
+
+        return True
+
+    def _check_resource_policy(self, policy, method, keys):
+        """Checks the policy for a specific resource and method.
+
+        This will grab the resource policy for the given policy ID,
+        and see if a given method can be performed on that resource,
+        without factoring in any global policy rules.
+
+        If the method is allowed and restrict_ids is True, this will then
+        check if the resource should be blocked based on the ID.
+
+        In case of a conflict, blocked policies always trump allowed
+        policies.
+        """
+        for key in keys:
+            sub_policy = policy.get(key)
+
+            if sub_policy:
+                # We first want to check the specific values, to see if they've
+                # been singled out. If not found, we'll check the wildcards.
+                #
+                # Blocked values always take precedence over allowed values.
+                allowed = sub_policy.get('allow', [])
+                blocked = sub_policy.get('block', [])
+
+                if method in blocked:
+                    return False
+                elif method in allowed:
+                    return True
+                elif '*' in blocked:
+                    return False
+                elif '*' in allowed:
+                    return True
+
+        return None
+
     def _get_api_token_for_request(self, request):
         webapi_token = getattr(request, '_webapi_token', None)
 
@@ -173,6 +265,45 @@ class WebAPIResource(DjbletsWebAPIResource):
 
         return webapi_token
 
+    def _get_queryset(self, request, is_list=False, *args, **kwargs):
+        """Returns the queryset for the resource.
+
+        This is a specialization of the Djblets WebAPIResource._get_queryset(),
+        which imposes further restrictions on the queryset results if using
+        a WebAPIToken for authentication that defines a policy.
+
+        Any items in the queryset that are denied by the policy will be
+        excluded from the results.
+        """
+        queryset = super(WebAPIResource, self)._get_queryset(
+            request, is_list=is_list, *args, **kwargs)
+
+        if is_list:
+            # We'll need to filter the list of results down to exclude any
+            # that are blocked for GET access by the token policy.
+            webapi_token = self._get_api_token_for_request(request)
+
+            if webapi_token:
+                resources_policy = webapi_token.policy.get('resources', {})
+                resource_policy = resources_policy.get(self.policy_id)
+
+                if resource_policy:
+                    resource_ids = [
+                        resource_id
+                        for resource_id in six.iterkeys(resource_policy)
+                        if (resource_id != '*' and
+                            not self._check_resource_policy(
+                                resources_policy, self.policy_id, 'GET',
+                                resource_id, True))
+                    ]
+
+                    if resource_ids:
+                        queryset = queryset.exclude(**{
+                            self.model_object_key + '__in': resource_ids,
+                        })
+
+        return queryset
+
     def _get_resource_url(self, name, local_site_name=None, request=None,
                           **kwargs):
         return local_site_reverse(
diff --git a/reviewboard/webapi/resources/draft_filediff.py b/reviewboard/webapi/resources/draft_filediff.py
index 501c14317e418ac925c48a06e8f699169b76234c..0337db508c9c8fd40ee2fa76361275aa8c8b1742 100644
--- a/reviewboard/webapi/resources/draft_filediff.py
+++ b/reviewboard/webapi/resources/draft_filediff.py
@@ -22,6 +22,7 @@ class DraftFileDiffResource(FileDiffResource):
     applies to exactly one file on a repository.
     """
     name = 'draft_file'
+    policy_id = 'draft_file_diff'
     uri_name = 'files'
     allowed_methods = ('GET', 'PUT')
     item_result_key = 'file'
diff --git a/reviewboard/webapi/resources/filediff.py b/reviewboard/webapi/resources/filediff.py
index ff87c9653169390d2b5d8f7b216337efc39b1dfc..c3565c87572c7905eb5028642e859685a3cb9403 100644
--- a/reviewboard/webapi/resources/filediff.py
+++ b/reviewboard/webapi/resources/filediff.py
@@ -33,6 +33,7 @@ class FileDiffResource(WebAPIResource):
     """
     model = FileDiff
     name = 'file'
+    policy_id = 'file_diff'
     allowed_methods = ('GET', 'PUT')
     fields = {
         'id': {
diff --git a/reviewboard/webapi/resources/filediff_comment.py b/reviewboard/webapi/resources/filediff_comment.py
index eb9a7b12021902ed24168906133fc0d7ec15b6e3..8fae4ca619c252b47d2f4a533fbda99c16fc33ef 100644
--- a/reviewboard/webapi/resources/filediff_comment.py
+++ b/reviewboard/webapi/resources/filediff_comment.py
@@ -21,6 +21,7 @@ class FileDiffCommentResource(BaseDiffCommentResource):
     possible values listed in the ``text_type`` field below.
     """
     allowed_methods = ('GET',)
+    policy_id = 'diff_comment'
     model_parent_key = 'filediff'
     uri_object_key = None
 
diff --git a/reviewboard/webapi/resources/repository_branches.py b/reviewboard/webapi/resources/repository_branches.py
index 9545a7b42b3998d5ca43ae41f514da7746a2fa51..4b68ea529d57c5013426a124e5baff6447da1364 100644
--- a/reviewboard/webapi/resources/repository_branches.py
+++ b/reviewboard/webapi/resources/repository_branches.py
@@ -33,6 +33,7 @@ class RepositoryBranchesResource(WebAPIResource):
     This is not available for all types of repositories.
     """
     name = 'branches'
+    policy_id = 'repository_branches'
     singleton = True
     allowed_methods = ('GET',)
     mimetype_item_resource_name = 'repository-branches'
diff --git a/reviewboard/webapi/resources/repository_commits.py b/reviewboard/webapi/resources/repository_commits.py
index ba657111b7e5d50b873594be076127c63d35da93..a89ca199f60968a44ea109a478417901233e50f5 100644
--- a/reviewboard/webapi/resources/repository_commits.py
+++ b/reviewboard/webapi/resources/repository_commits.py
@@ -45,6 +45,7 @@ class RepositoryCommitsResource(WebAPIResource):
     This is not available for all types of repositories.
     """
     name = 'commits'
+    policy_id = 'repository_commits'
     singleton = True
     allowed_methods = ('GET',)
     mimetype_item_resource_name = 'repository-commits'
diff --git a/reviewboard/webapi/resources/repository_info.py b/reviewboard/webapi/resources/repository_info.py
index 4fd771c11f38afe7be914fb6a976a081c2a0013c..4f60dee48422488539b4b9e994fe2273fbad3ccc 100644
--- a/reviewboard/webapi/resources/repository_info.py
+++ b/reviewboard/webapi/resources/repository_info.py
@@ -19,6 +19,7 @@ class RepositoryInfoResource(WebAPIResource):
     will be specific to that type of repository.
     """
     name = 'info'
+    policy_id = 'repository_info'
     singleton = True
     allowed_methods = ('GET',)
     mimetype_item_resource_name = 'repository-info'
diff --git a/reviewboard/webapi/resources/review_diff_comment.py b/reviewboard/webapi/resources/review_diff_comment.py
index 96903a7fcaa9e29283be81accb97f393d047282d..69cd46b09623b6d0d03aa410e3b6cdcdffe0d991 100644
--- a/reviewboard/webapi/resources/review_diff_comment.py
+++ b/reviewboard/webapi/resources/review_diff_comment.py
@@ -30,6 +30,7 @@ class ReviewDiffCommentResource(BaseDiffCommentResource):
     possible values listed in the ``text_type`` field below.
     """
     allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
+    policy_id = 'review_diff_comment'
     model_parent_key = 'review'
 
     mimetype_list_resource_name = 'review-diff-comments'
diff --git a/reviewboard/webapi/resources/review_file_attachment_comment.py b/reviewboard/webapi/resources/review_file_attachment_comment.py
index 50142c2757612ff75c8afc4cf91db6fac2b6c550..8f3d037c1577f55fbf30d1f3d37b57c03cdb2aeb 100644
--- a/reviewboard/webapi/resources/review_file_attachment_comment.py
+++ b/reviewboard/webapi/resources/review_file_attachment_comment.py
@@ -30,6 +30,7 @@ class ReviewFileAttachmentCommentResource(BaseFileAttachmentCommentResource):
     possible values listed in the ``text_type`` field below.
     """
     allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
+    policy_id = 'review_file_attachment_comment'
     model_parent_key = 'review'
 
     def get_queryset(self, request, review_id, *args, **kwargs):
diff --git a/reviewboard/webapi/resources/review_group_user.py b/reviewboard/webapi/resources/review_group_user.py
index ab538f48cc4cd392c4a7ddff18062ae081ab31eb..e19afc22a066ad480ef269d68e9bfc0c413ab79a 100644
--- a/reviewboard/webapi/resources/review_group_user.py
+++ b/reviewboard/webapi/resources/review_group_user.py
@@ -22,6 +22,8 @@ class ReviewGroupUserResource(UserResource):
     """Provides information on users that are members of a review group."""
     allowed_methods = ('GET', 'POST', 'DELETE')
 
+    policy_id = 'review_group_user'
+
     def get_queryset(self, request, group_name, local_site_name=None,
                      *args, **kwargs):
         group = Group.objects.get(name=group_name,
diff --git a/reviewboard/webapi/resources/review_reply.py b/reviewboard/webapi/resources/review_reply.py
index 04f60ab2c49a15d82a7fd0299a660f1f54b19ced..2b7bb22a4ab40d7068bbbe88075a68f761cac65b 100644
--- a/reviewboard/webapi/resources/review_reply.py
+++ b/reviewboard/webapi/resources/review_reply.py
@@ -36,6 +36,7 @@ class ReviewReplyResource(BaseReviewResource):
     """
     name = 'reply'
     name_plural = 'replies'
+    policy_id = 'review_reply'
     fields = {
         'body_bottom': {
             'type': six.text_type,
diff --git a/reviewboard/webapi/resources/review_reply_diff_comment.py b/reviewboard/webapi/resources/review_reply_diff_comment.py
index fb38f818cd148dfb572372710426c71d61537cd7..be104451c46b5bdb10f2750d151d558b8222e15f 100644
--- a/reviewboard/webapi/resources/review_reply_diff_comment.py
+++ b/reviewboard/webapi/resources/review_reply_diff_comment.py
@@ -32,6 +32,7 @@ class ReviewReplyDiffCommentResource(BaseDiffCommentResource):
     possible values listed in the ``text_type`` field below.
     """
     allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
+    policy_id = 'review_reply_diff_comment'
     model_parent_key = 'review'
     fields = dict({
         'reply_to': {
diff --git a/reviewboard/webapi/resources/review_reply_draft.py b/reviewboard/webapi/resources/review_reply_draft.py
index 74107558b3b8892dcaf9f6ae250c345b89425652..6db84ff529d98df42381f7b1fac80434507ab630 100644
--- a/reviewboard/webapi/resources/review_reply_draft.py
+++ b/reviewboard/webapi/resources/review_reply_draft.py
@@ -16,6 +16,7 @@ class ReviewReplyDraftResource(WebAPIResource):
     clients can discover the proper location.
     """
     name = 'reply_draft'
+    policy_id = 'review_reply_draft'
     singleton = True
     uri_name = 'draft'
 
diff --git a/reviewboard/webapi/resources/review_reply_file_attachment_comment.py b/reviewboard/webapi/resources/review_reply_file_attachment_comment.py
index 9628b4be092abb09ddf053ae893d563cd51a744a..d3462427a557992667799b7c7ff10750cac33b5b 100644
--- a/reviewboard/webapi/resources/review_reply_file_attachment_comment.py
+++ b/reviewboard/webapi/resources/review_reply_file_attachment_comment.py
@@ -35,6 +35,7 @@ class ReviewReplyFileAttachmentCommentResource(
     possible values listed in the ``text_type`` field below.
     """
     allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
+    policy_id = 'review_reply_file_attachment_comment'
     model_parent_key = 'review'
     fields = dict({
         'reply_to': {
diff --git a/reviewboard/webapi/resources/review_reply_screenshot_comment.py b/reviewboard/webapi/resources/review_reply_screenshot_comment.py
index 32d66b45d936e57f6797503f880202d147070aaf..b313eb6694454b5da5bd084d5227549ab226a770 100644
--- a/reviewboard/webapi/resources/review_reply_screenshot_comment.py
+++ b/reviewboard/webapi/resources/review_reply_screenshot_comment.py
@@ -33,6 +33,7 @@ class ReviewReplyScreenshotCommentResource(BaseScreenshotCommentResource):
     possible values listed in the ``text_type`` field below.
     """
     allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
+    policy_id = 'review_reply_screenshot_comment'
     model_parent_key = 'review'
     fields = dict({
         'reply_to': {
diff --git a/reviewboard/webapi/resources/review_request_draft.py b/reviewboard/webapi/resources/review_request_draft.py
index 787a3fe9c2b2d8d4066addcf76a2e1b7ee9680ea..f27629c45659e490b7fa3cf904f4f53fab31676a 100644
--- a/reviewboard/webapi/resources/review_request_draft.py
+++ b/reviewboard/webapi/resources/review_request_draft.py
@@ -51,6 +51,7 @@ class ReviewRequestDraftResource(MarkdownFieldsMixin, WebAPIResource):
     """
     model = ReviewRequestDraft
     name = 'draft'
+    policy_id = 'review_request_draft'
     singleton = True
     model_parent_key = 'review_request'
     last_modified_field = 'last_updated'
diff --git a/reviewboard/webapi/resources/review_request_last_update.py b/reviewboard/webapi/resources/review_request_last_update.py
index 4742afbb09d2f7095e8fa4a55fbbf453762766a3..e18803ed95e0926fb7656bb276026c40ceb660cc 100644
--- a/reviewboard/webapi/resources/review_request_last_update.py
+++ b/reviewboard/webapi/resources/review_request_last_update.py
@@ -21,6 +21,7 @@ class ReviewRequestLastUpdateResource(WebAPIResource):
     made.
     """
     name = 'last_update'
+    policy_id = 'review_request_last_update'
     singleton = True
     allowed_methods = ('GET',)
 
diff --git a/reviewboard/webapi/resources/review_screenshot_comment.py b/reviewboard/webapi/resources/review_screenshot_comment.py
index 8d4484573202043871060bdc06c9a61443acf210..a4432f3191d32920506e6f3896f54c97de6f918d 100644
--- a/reviewboard/webapi/resources/review_screenshot_comment.py
+++ b/reviewboard/webapi/resources/review_screenshot_comment.py
@@ -30,6 +30,7 @@ class ReviewScreenshotCommentResource(BaseScreenshotCommentResource):
     possible values listed in the ``text_type`` field below.
     """
     allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
+    policy_id = 'review_screenshot_comment'
     model_parent_key = 'review'
 
     def get_queryset(self, request, review_id, *args, **kwargs):
diff --git a/reviewboard/webapi/resources/root.py b/reviewboard/webapi/resources/root.py
index 87663a9591d7d706be973766624d02f1fa47ca19..d19cb1dd0ab520e7f8d6b0f6d19a21bc0951a5be 100644
--- a/reviewboard/webapi/resources/root.py
+++ b/reviewboard/webapi/resources/root.py
@@ -6,10 +6,10 @@ from djblets.webapi.resources import RootResource as DjbletsRootResource
 from reviewboard.webapi.server_info import get_server_info
 from reviewboard.webapi.decorators import (webapi_check_login_required,
                                            webapi_check_local_site)
-from reviewboard.webapi.resources import resources
+from reviewboard.webapi.resources import WebAPIResource, resources
 
 
-class RootResource(DjbletsRootResource):
+class RootResource(WebAPIResource, DjbletsRootResource):
     """Links to all the main resources, including URI templates to resources
     anywhere in the tree.
 
diff --git a/reviewboard/webapi/resources/server_info.py b/reviewboard/webapi/resources/server_info.py
index de0de8999579b053f256403d79e0fb82dfa6de5e..a5ea4e4603f5270d797a45d39b5671a051f53b8c 100644
--- a/reviewboard/webapi/resources/server_info.py
+++ b/reviewboard/webapi/resources/server_info.py
@@ -19,6 +19,7 @@ class ServerInfoResource(WebAPIResource):
     This is deprecated in favor of the data in the root resource.
     """
     name = 'info'
+    policy_id = 'server_info'
     singleton = True
     mimetype_item_resource_name = 'server-info'
 
diff --git a/reviewboard/webapi/tests/test_api_policy.py b/reviewboard/webapi/tests/test_api_policy.py
new file mode 100644
index 0000000000000000000000000000000000000000..fde11682eebc44eb6f6ce3ea3758e908311265be
--- /dev/null
+++ b/reviewboard/webapi/tests/test_api_policy.py
@@ -0,0 +1,305 @@
+from __future__ import unicode_literals
+
+from django.utils import six
+
+from reviewboard.testing import TestCase
+from reviewboard.webapi.base import WebAPIResource
+
+
+class PolicyTestResource(WebAPIResource):
+    policy_id = 'test'
+
+
+class APIPolicyTests(TestCase):
+    """Tests API policy through WebAPITokens."""
+    def setUp(self):
+        super(APIPolicyTests, self).setUp()
+
+        self.resource = PolicyTestResource()
+
+    def test_default_policy(self):
+        """Testing API policy enforcement with default policy"""
+        self.assert_policy(
+            {},
+            allowed_methods=['HEAD', 'GET', 'POST', 'PATCH', 'PUT', 'DELETE'])
+
+    def test_global_allow_all(self):
+        """Testing API policy enforcement with *.allow=*"""
+        self.assert_policy(
+            {
+                '*': {
+                    'allow': ['*'],
+                }
+            },
+            allowed_methods=['HEAD', 'GET', 'POST', 'PATCH', 'PUT', 'DELETE'])
+
+    def test_global_block_all(self):
+        """Testing API policy enforcement with *.block=*"""
+        self.assert_policy(
+            {
+                '*': {
+                    'block': ['*'],
+                }
+            },
+            blocked_methods=['HEAD', 'GET', 'POST', 'PATCH', 'PUT', 'DELETE'])
+
+    def test_global_block_all_and_resource_allow_all(self):
+        """Testing API policy enforcement with *.block=* and
+        <resource>.*.allow=*
+        """
+        self.assert_policy(
+            {
+                '*': {
+                    'block': ['*'],
+                },
+                'test': {
+                    '*': {
+                        'allow': ['*'],
+                    },
+                }
+            },
+            allowed_methods=['HEAD', 'GET', 'POST', 'PATCH', 'PUT', 'DELETE'])
+
+    def test_global_allow_all_and_resource_block_all(self):
+        """Testing API policy enforcement with *.allow=* and
+        <resource>.*.block=*
+        """
+        self.assert_policy(
+            {
+                '*': {
+                    'allow': ['*'],
+                },
+                'test': {
+                    '*': {
+                        'block': ['*'],
+                    },
+                }
+            },
+            blocked_methods=['HEAD', 'GET', 'POST', 'PATCH', 'PUT', 'DELETE'])
+
+    def test_global_block_all_and_resource_all_allow_methods(self):
+        """Testing API policy enforcement with *.block=* and
+        <resource>.*.allow=[methods]
+        """
+        self.assert_policy(
+            {
+                '*': {
+                    'block': ['*'],
+                },
+                'test': {
+                    '*': {
+                        'allow': ['GET', 'PUT'],
+                    },
+                }
+            },
+            allowed_methods=['GET', 'PUT'],
+            blocked_methods=['HEAD', 'POST', 'PATCH', 'DELETE'])
+
+    def test_global_allow_all_and_resource_all_block_specific(self):
+        """Testing API policy enforcement with *.allow=* and
+        <resource>.*.block=[methods]
+        """
+        self.assert_policy(
+            {
+                '*': {
+                    'allow': ['*'],
+                },
+                'test': {
+                    '*': {
+                        'block': ['GET', 'PUT'],
+                    },
+                }
+            },
+            allowed_methods=['HEAD', 'POST', 'PATCH', 'DELETE'],
+            blocked_methods=['GET', 'PUT'])
+
+    def test_resource_block_all_and_allow_methods(self):
+        """Testing API policy enforcement with <resource>.*.block=* and
+        <resource>.*.allow=[methods] for specific methods
+        """
+        self.assert_policy(
+            {
+                'test': {
+                    '*': {
+                        'block': ['*'],
+                        'allow': ['GET', 'PUT'],
+                    }
+                }
+            },
+            allowed_methods=['GET', 'PUT'],
+            blocked_methods=['HEAD', 'POST', 'PATCH', 'DELETE'])
+
+    def test_resource_allow_all_and_block_methods(self):
+        """Testing API policy enforcement with <resource>.*.allow=* and
+        <resource>.*.block=[methods] for specific methods
+        """
+        self.assert_policy(
+            {
+                'test': {
+                    '*': {
+                        'allow': ['*'],
+                        'block': ['GET', 'PUT'],
+                    },
+                }
+            },
+            allowed_methods=['HEAD', 'POST', 'DELETE'],
+            blocked_methods=['GET', 'PUT'])
+
+    def test_id_allow_all(self):
+        """Testing API policy enforcement with <resource>.<id>.allow=*"""
+        self.assert_policy(
+            {
+                'test': {
+                    '42': {
+                        'allow': ['*'],
+                    }
+                }
+            },
+            resource_id=42,
+            allowed_methods=['HEAD', 'GET', 'POST', 'PUT', 'DELETE'])
+
+    def test_id_block_all(self):
+        """Testing API policy enforcement with <resource>.<id>.block=*"""
+        policy = {
+            'test': {
+                '42': {
+                    'block': ['*'],
+                }
+            }
+        }
+
+        self.assert_policy(
+            policy,
+            resource_id=42,
+            blocked_methods=['HEAD', 'GET', 'POST', 'PUT', 'DELETE'])
+
+        self.assert_policy(
+            policy,
+            resource_id=100,
+            allowed_methods=['HEAD', 'GET', 'POST', 'PUT', 'DELETE'])
+
+    def test_resource_block_all_and_id_allow_all(self):
+        """Testing API policy enforcement with <resource>.*.block=* and
+        <resource>.<id>.allow=*
+        """
+        policy = {
+            'test': {
+                '*': {
+                    'block': ['*'],
+                },
+                '42': {
+                    'allow': ['*'],
+                }
+            }
+        }
+
+        self.assert_policy(
+            policy,
+            resource_id=42,
+            allowed_methods=['HEAD', 'GET', 'POST', 'PUT', 'DELETE'])
+
+        self.assert_policy(
+            policy,
+            resource_id=100,
+            blocked_methods=['HEAD', 'GET', 'POST', 'PUT', 'DELETE'])
+
+    def test_resource_allow_all_and_id_block_all(self):
+        """Testing API policy enforcement with <resource>.<id>.allow=* and
+        <resource>.<id>.block=*
+        """
+        policy = {
+            'test': {
+                '*': {
+                    'allow': ['*'],
+                },
+                '42': {
+                    'block': ['*'],
+                }
+            }
+        }
+
+        self.assert_policy(
+            policy,
+            resource_id=42,
+            blocked_methods=['HEAD', 'GET', 'POST', 'PUT', 'DELETE'])
+
+        self.assert_policy(
+            policy,
+            resource_id=100,
+            allowed_methods=['HEAD', 'GET', 'POST', 'PUT', 'DELETE'])
+
+    def test_global_block_all_and_id_allow_all(self):
+        """Testing API policy enforcement with *.<id>.block=* and
+        <resource>.<id>.allow=*
+        """
+        self.assert_policy(
+            {
+                '*': {
+                    'block': ['*'],
+                },
+                'test': {
+                    '42': {
+                        'allow': ['*'],
+                    }
+                }
+            },
+            resource_id=42,
+            allowed_methods=['HEAD', 'GET', 'POST', 'PUT', 'DELETE'])
+
+    def test_global_allow_all_and_id_block_all(self):
+        """Testing API policy enforcement with *.<id>.allow=* and
+        <resource>.<id>.block=*"""
+        policy = {
+            '*': {
+                'allow': ['*'],
+            },
+            'test': {
+                '42': {
+                    'block': ['*'],
+                }
+            }
+        }
+
+        self.assert_policy(
+            policy,
+            resource_id=42,
+            blocked_methods=['HEAD', 'GET', 'POST', 'PUT', 'DELETE'])
+
+        self.assert_policy(
+            policy,
+            resource_id=100,
+            allowed_methods=['HEAD', 'GET', 'POST', 'PUT', 'DELETE'])
+
+    def test_policy_methods_conflict(self):
+        """Testing API policy enforcement with methods conflict"""
+        self.assert_policy(
+            {
+                'test': {
+                    '*': {
+                        'allow': ['*'],
+                        'block': ['*'],
+                    },
+                }
+            },
+            blocked_methods=['HEAD', 'GET', 'POST', 'PUT', 'DELETE'])
+
+    def assert_policy(self, policy, allowed_methods=[], blocked_methods=[],
+                      resource_id=None):
+        if resource_id is not None:
+            resource_id = six.text_type(resource_id)
+
+        for method in allowed_methods:
+            allowed = self.resource.is_resource_method_allowed(
+                policy, method, resource_id)
+
+            if not allowed:
+                self.fail('Expected %s to be allowed, but was blocked'
+                          % method)
+
+        for method in blocked_methods:
+            allowed = self.resource.is_resource_method_allowed(
+                policy, method, resource_id)
+
+            if allowed:
+                self.fail('Expected %s to be blocked, but was allowed'
+                          % method)
